"""
Deep behavioral tests for NewsAnalyzer helper methods.
Tests _validate_news_item, _count_categories, _summarize_impact,
_prepare_snippets, _empty_analysis, and LLM-dependent methods with mocks.
"""

from datetime import datetime
from unittest.mock import Mock, patch

from local_deep_research.news.core.news_analyzer import NewsAnalyzer


def _make_analyzer(llm_client=None):
    """Create a NewsAnalyzer with optional mock LLM."""
    with patch(
        "local_deep_research.news.core.news_analyzer.get_llm"
    ) as mock_get:
        mock_get.return_value = llm_client
        return NewsAnalyzer(llm_client=llm_client)


# --- _validate_news_item ---


class TestValidateNewsItem:
    """Tests for news item validation."""

    def test_valid_item(self):
        analyzer = _make_analyzer()
        assert analyzer._validate_news_item(
            {"headline": "Test News", "summary": "A summary here"}
        )

    def test_missing_headline(self):
        analyzer = _make_analyzer()
        assert not analyzer._validate_news_item({"summary": "A summary"})

    def test_missing_summary(self):
        analyzer = _make_analyzer()
        assert not analyzer._validate_news_item({"headline": "Test"})

    def test_empty_headline(self):
        analyzer = _make_analyzer()
        assert not analyzer._validate_news_item(
            {"headline": "", "summary": "A summary"}
        )

    def test_empty_summary(self):
        analyzer = _make_analyzer()
        assert not analyzer._validate_news_item(
            {"headline": "Test", "summary": ""}
        )

    def test_extra_fields_ok(self):
        analyzer = _make_analyzer()
        assert analyzer._validate_news_item(
            {
                "headline": "Test",
                "summary": "Summary",
                "category": "Tech",
                "impact_score": 8,
                "extra": "data",
            }
        )

    def test_empty_dict(self):
        analyzer = _make_analyzer()
        assert not analyzer._validate_news_item({})

    def test_none_headline(self):
        analyzer = _make_analyzer()
        assert not analyzer._validate_news_item(
            {"headline": None, "summary": "Summary"}
        )

    def test_none_summary(self):
        analyzer = _make_analyzer()
        assert not analyzer._validate_news_item(
            {"headline": "Test", "summary": None}
        )


# --- _count_categories ---


class TestCountCategories:
    """Tests for category counting."""

    def test_empty_list(self):
        analyzer = _make_analyzer()
        assert analyzer._count_categories([]) == {}

    def test_single_category(self):
        analyzer = _make_analyzer()
        items = [{"category": "Tech"}]
        assert analyzer._count_categories(items) == {"Tech": 1}

    def test_multiple_same_category(self):
        analyzer = _make_analyzer()
        items = [
            {"category": "Tech"},
            {"category": "Tech"},
            {"category": "Tech"},
        ]
        assert analyzer._count_categories(items) == {"Tech": 3}

    def test_multiple_categories(self):
        analyzer = _make_analyzer()
        items = [
            {"category": "Tech"},
            {"category": "Science"},
            {"category": "Tech"},
            {"category": "Politics"},
        ]
        result = analyzer._count_categories(items)
        assert result == {"Tech": 2, "Science": 1, "Politics": 1}

    def test_missing_category_defaults_to_other(self):
        analyzer = _make_analyzer()
        items = [{"headline": "No category"}]
        result = analyzer._count_categories(items)
        assert result == {"Other": 1}

    def test_mixed_with_missing(self):
        analyzer = _make_analyzer()
        items = [
            {"category": "Tech"},
            {"headline": "No cat"},
            {"category": "Tech"},
        ]
        result = analyzer._count_categories(items)
        assert result == {"Tech": 2, "Other": 1}


# --- _summarize_impact ---


class TestSummarizeImpact:
    """Tests for impact summary calculation."""

    def test_empty_list(self):
        analyzer = _make_analyzer()
        result = analyzer._summarize_impact([])
        assert result == {"average": 0, "high_impact_count": 0}

    def test_single_item(self):
        analyzer = _make_analyzer()
        result = analyzer._summarize_impact([{"impact_score": 7}])
        assert result["average"] == 7
        assert result["max"] == 7
        assert result["min"] == 7

    def test_average_calculation(self):
        analyzer = _make_analyzer()
        items = [
            {"impact_score": 4},
            {"impact_score": 6},
            {"impact_score": 8},
        ]
        result = analyzer._summarize_impact(items)
        assert result["average"] == 6.0

    def test_high_impact_count_threshold_8(self):
        analyzer = _make_analyzer()
        items = [
            {"impact_score": 7},
            {"impact_score": 8},
            {"impact_score": 9},
            {"impact_score": 10},
        ]
        result = analyzer._summarize_impact(items)
        assert result["high_impact_count"] == 3  # 8, 9, 10

    def test_no_high_impact(self):
        analyzer = _make_analyzer()
        items = [{"impact_score": 3}, {"impact_score": 5}]
        result = analyzer._summarize_impact(items)
        assert result["high_impact_count"] == 0

    def test_max_score(self):
        analyzer = _make_analyzer()
        items = [{"impact_score": 2}, {"impact_score": 9}, {"impact_score": 5}]
        result = analyzer._summarize_impact(items)
        assert result["max"] == 9

    def test_min_score(self):
        analyzer = _make_analyzer()
        items = [{"impact_score": 2}, {"impact_score": 9}, {"impact_score": 5}]
        result = analyzer._summarize_impact(items)
        assert result["min"] == 2

    def test_missing_impact_defaults_to_5(self):
        analyzer = _make_analyzer()
        items = [{"headline": "No score"}]
        result = analyzer._summarize_impact(items)
        assert result["average"] == 5

    def test_all_same_score(self):
        analyzer = _make_analyzer()
        items = [{"impact_score": 6}] * 5
        result = analyzer._summarize_impact(items)
        assert result["average"] == 6.0
        assert result["max"] == 6
        assert result["min"] == 6


# --- _prepare_snippets ---


class TestPrepareSnippets:
    """Tests for snippet preparation."""

    def test_empty_results(self):
        analyzer = _make_analyzer()
        assert analyzer._prepare_snippets([]) == ""

    def test_includes_title(self):
        analyzer = _make_analyzer()
        results = [{"title": "Test Title"}]
        snippets = analyzer._prepare_snippets(results)
        assert "Test Title" in snippets

    def test_includes_url(self):
        analyzer = _make_analyzer()
        results = [{"url": "https://example.com"}]
        snippets = analyzer._prepare_snippets(results)
        assert "https://example.com" in snippets

    def test_includes_snippet_text(self):
        analyzer = _make_analyzer()
        results = [{"snippet": "This is a snippet about news"}]
        snippets = analyzer._prepare_snippets(results)
        assert "This is a snippet about news" in snippets

    def test_uses_content_when_no_snippet(self):
        analyzer = _make_analyzer()
        results = [{"content": "This is content"}]
        snippets = analyzer._prepare_snippets(results)
        assert "This is content" in snippets

    def test_snippet_truncated_at_200_chars(self):
        analyzer = _make_analyzer()
        long_snippet = "x" * 500
        results = [{"snippet": long_snippet}]
        snippets = analyzer._prepare_snippets(results)
        # Should contain truncated snippet (200 chars) + "..."
        assert "..." in snippets

    def test_content_truncated_at_200_chars(self):
        analyzer = _make_analyzer()
        long_content = "y" * 500
        results = [{"content": long_content}]
        snippets = analyzer._prepare_snippets(results)
        assert "..." in snippets

    def test_numbered_results(self):
        analyzer = _make_analyzer()
        results = [
            {"title": "First"},
            {"title": "Second"},
            {"title": "Third"},
        ]
        snippets = analyzer._prepare_snippets(results)
        assert "[1]" in snippets
        assert "[2]" in snippets
        assert "[3]" in snippets

    def test_all_fields_combined(self):
        analyzer = _make_analyzer()
        results = [
            {
                "title": "Breaking News",
                "url": "https://news.example.com",
                "snippet": "Important development today",
            }
        ]
        snippets = analyzer._prepare_snippets(results)
        assert "Breaking News" in snippets
        assert "https://news.example.com" in snippets
        assert "Important development today" in snippets

    def test_empty_fields_skipped(self):
        analyzer = _make_analyzer()
        results = [{"title": "", "url": "", "snippet": ""}]
        snippets = analyzer._prepare_snippets(results)
        # Empty strings are falsy, so they should be skipped
        assert "Title:" not in snippets


# --- _empty_analysis ---


class TestEmptyAnalysis:
    """Tests for empty analysis structure."""

    def test_has_items_key(self):
        analyzer = _make_analyzer()
        result = analyzer._empty_analysis()
        assert result["items"] == []

    def test_has_item_count_zero(self):
        analyzer = _make_analyzer()
        result = analyzer._empty_analysis()
        assert result["item_count"] == 0

    def test_has_big_picture_empty(self):
        analyzer = _make_analyzer()
        result = analyzer._empty_analysis()
        assert result["big_picture"] == ""

    def test_has_watch_for_empty(self):
        analyzer = _make_analyzer()
        result = analyzer._empty_analysis()
        assert result["watch_for"] == []

    def test_has_patterns_empty(self):
        analyzer = _make_analyzer()
        result = analyzer._empty_analysis()
        assert result["patterns"] == ""

    def test_has_topics_empty(self):
        analyzer = _make_analyzer()
        result = analyzer._empty_analysis()
        assert result["topics"] == []

    def test_has_categories_empty(self):
        analyzer = _make_analyzer()
        result = analyzer._empty_analysis()
        assert result["categories"] == {}

    def test_has_impact_summary(self):
        analyzer = _make_analyzer()
        result = analyzer._empty_analysis()
        assert result["impact_summary"]["average"] == 0
        assert result["impact_summary"]["high_impact_count"] == 0

    def test_has_timestamp(self):
        analyzer = _make_analyzer()
        result = analyzer._empty_analysis()
        assert "timestamp" in result
        # Should be parseable ISO format
        datetime.fromisoformat(result["timestamp"])

    def test_all_keys_present(self):
        analyzer = _make_analyzer()
        result = analyzer._empty_analysis()
        expected_keys = {
            "items",
            "item_count",
            "big_picture",
            "watch_for",
            "patterns",
            "topics",
            "categories",
            "impact_summary",
            "timestamp",
        }
        assert set(result.keys()) == expected_keys


# --- analyze_news ---


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

    def test_empty_input_returns_empty_analysis(self):
        analyzer = _make_analyzer()
        result = analyzer.analyze_news([])
        assert result["items"] == []
        assert result["item_count"] == 0

    def test_none_input_returns_empty_analysis(self):
        analyzer = _make_analyzer()
        result = analyzer.analyze_news(None)
        assert result["item_count"] == 0


# --- extract_news_items ---


class TestExtractNewsItems:
    """Tests for news item extraction."""

    def test_no_llm_returns_empty(self):
        analyzer = _make_analyzer(llm_client=None)
        result = analyzer.extract_news_items([{"title": "Test"}])
        assert result == []

    def test_with_llm_parses_json_response(self):
        mock_llm = Mock()
        mock_llm.invoke.return_value = Mock(
            content='[{"headline": "AI Breakthrough", "summary": "Major discovery"}]'
        )
        analyzer = _make_analyzer(llm_client=mock_llm)
        result = analyzer.extract_news_items([{"title": "Source"}])
        assert len(result) == 1
        assert result[0]["headline"] == "AI Breakthrough"
        assert "id" in result[0]  # ID should be generated

    def test_invalid_json_returns_empty(self):
        mock_llm = Mock()
        mock_llm.invoke.return_value = Mock(content="not valid json at all")
        analyzer = _make_analyzer(llm_client=mock_llm)
        result = analyzer.extract_news_items([{"title": "Source"}])
        assert result == []

    def test_respects_max_items(self):
        mock_llm = Mock()
        items = [
            {"headline": f"News {i}", "summary": f"Summary {i}"}
            for i in range(20)
        ]
        import json

        mock_llm.invoke.return_value = Mock(content=json.dumps(items))
        analyzer = _make_analyzer(llm_client=mock_llm)
        result = analyzer.extract_news_items([{"title": "Source"}], max_items=5)
        assert len(result) <= 5

    def test_filters_invalid_items(self):
        mock_llm = Mock()
        items = [
            {"headline": "Valid", "summary": "Summary"},
            {"headline": "", "summary": "No headline"},
            {"headline": "Also Valid", "summary": "Another summary"},
        ]
        import json

        mock_llm.invoke.return_value = Mock(content=json.dumps(items))
        analyzer = _make_analyzer(llm_client=mock_llm)
        result = analyzer.extract_news_items([{"title": "Source"}])
        assert len(result) == 2

    def test_llm_exception_returns_empty(self):
        mock_llm = Mock()
        mock_llm.invoke.side_effect = RuntimeError("LLM failed")
        analyzer = _make_analyzer(llm_client=mock_llm)
        result = analyzer.extract_news_items([{"title": "Source"}])
        assert result == []


# --- generate_big_picture ---


class TestGenerateBigPicture:
    """Tests for big picture generation."""

    def test_no_llm_returns_empty(self):
        analyzer = _make_analyzer(llm_client=None)
        assert analyzer.generate_big_picture([{"headline": "Test"}]) == ""

    def test_empty_items_returns_empty(self):
        mock_llm = Mock()
        analyzer = _make_analyzer(llm_client=mock_llm)
        assert analyzer.generate_big_picture([]) == ""

    def test_with_llm_returns_content(self):
        mock_llm = Mock()
        mock_llm.invoke.return_value = Mock(
            content="  The big picture is clear.  "
        )
        analyzer = _make_analyzer(llm_client=mock_llm)
        result = analyzer.generate_big_picture(
            [{"headline": "News", "summary": "Sum"}]
        )
        assert result == "The big picture is clear."

    def test_llm_error_returns_empty(self):
        mock_llm = Mock()
        mock_llm.invoke.side_effect = RuntimeError("fail")
        analyzer = _make_analyzer(llm_client=mock_llm)
        result = analyzer.generate_big_picture([{"headline": "News"}])
        assert result == ""


# --- generate_watch_for ---


class TestGenerateWatchFor:
    """Tests for watch-for item generation."""

    def test_no_llm_returns_empty(self):
        analyzer = _make_analyzer(llm_client=None)
        assert analyzer.generate_watch_for([{"headline": "Test"}]) == []

    def test_empty_items_returns_empty(self):
        mock_llm = Mock()
        analyzer = _make_analyzer(llm_client=mock_llm)
        assert analyzer.generate_watch_for([]) == []

    def test_parses_bullet_points(self):
        mock_llm = Mock()
        mock_llm.invoke.return_value = Mock(
            content="- First item\n- Second item\n- Third item"
        )
        analyzer = _make_analyzer(llm_client=mock_llm)
        result = analyzer.generate_watch_for(
            [{"headline": "News", "is_developing": True}]
        )
        assert len(result) == 3
        assert result[0] == "First item"

    def test_strips_bullet_markers(self):
        mock_llm = Mock()
        mock_llm.invoke.return_value = Mock(
            content="* Item one\n- Item two\n\u2022 Item three"
        )
        analyzer = _make_analyzer(llm_client=mock_llm)
        result = analyzer.generate_watch_for(
            [{"headline": "News", "is_developing": True}]
        )
        assert all(not item.startswith(("-", "*", "\u2022")) for item in result)

    def test_max_5_items(self):
        mock_llm = Mock()
        lines = "\n".join([f"- Item {i}" for i in range(10)])
        mock_llm.invoke.return_value = Mock(content=lines)
        analyzer = _make_analyzer(llm_client=mock_llm)
        result = analyzer.generate_watch_for(
            [{"headline": "News", "is_developing": True}]
        )
        assert len(result) <= 5

    def test_filters_header_lines(self):
        mock_llm = Mock()
        mock_llm.invoke.return_value = Mock(
            content="WATCH FOR:\n- Real item\nWatch for:\n- Another item"
        )
        analyzer = _make_analyzer(llm_client=mock_llm)
        result = analyzer.generate_watch_for(
            [{"headline": "News", "is_developing": True}]
        )
        assert "WATCH FOR:" not in result
        assert "Watch for:" not in result

    def test_uses_developing_stories_first(self):
        mock_llm = Mock()
        mock_llm.invoke.return_value = Mock(content="- Watch this")
        analyzer = _make_analyzer(llm_client=mock_llm)
        items = [
            {
                "headline": "Developing",
                "is_developing": True,
                "summary": "ongoing",
            },
            {"headline": "Old", "is_developing": False, "summary": "done"},
        ]
        analyzer.generate_watch_for(items)
        # Verify LLM was called (developing stories used)
        mock_llm.invoke.assert_called_once()
        call_args = mock_llm.invoke.call_args[0][0]
        assert "Developing" in call_args

    def test_falls_back_to_first_5_when_no_developing(self):
        mock_llm = Mock()
        mock_llm.invoke.return_value = Mock(content="- Watch this")
        analyzer = _make_analyzer(llm_client=mock_llm)
        items = [
            {"headline": f"Story {i}", "is_developing": False}
            for i in range(10)
        ]
        analyzer.generate_watch_for(items)
        mock_llm.invoke.assert_called_once()

    def test_llm_error_returns_empty(self):
        mock_llm = Mock()
        mock_llm.invoke.side_effect = RuntimeError("fail")
        analyzer = _make_analyzer(llm_client=mock_llm)
        result = analyzer.generate_watch_for(
            [{"headline": "News", "is_developing": True}]
        )
        assert result == []


# --- generate_patterns ---


class TestGeneratePatterns:
    """Tests for pattern recognition."""

    def test_no_llm_returns_empty(self):
        analyzer = _make_analyzer(llm_client=None)
        assert analyzer.generate_patterns([{"headline": "Test"}]) == ""

    def test_empty_items_returns_empty(self):
        mock_llm = Mock()
        analyzer = _make_analyzer(llm_client=mock_llm)
        assert analyzer.generate_patterns([]) == ""

    def test_with_llm_returns_content(self):
        mock_llm = Mock()
        mock_llm.invoke.return_value = Mock(content="  Pattern detected.  ")
        analyzer = _make_analyzer(llm_client=mock_llm)
        items = [{"headline": "AI News", "category": "Tech"}]
        result = analyzer.generate_patterns(items)
        assert result == "Pattern detected."

    def test_llm_error_returns_empty(self):
        mock_llm = Mock()
        mock_llm.invoke.side_effect = RuntimeError("fail")
        analyzer = _make_analyzer(llm_client=mock_llm)
        result = analyzer.generate_patterns(
            [{"headline": "Test", "category": "Tech"}]
        )
        assert result == ""
