"""
Deep behavioral tests for news_analyzer.py pure logic methods.
Tests _validate_news_item, _count_categories, _summarize_impact,
_empty_analysis, _prepare_snippets, extract_topics dedup, and
watch_for bullet parsing patterns.
"""

from datetime import datetime, timezone


# --- _validate_news_item ---


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

    def _validate(self, item):
        required = ["headline", "summary"]
        return all(field in item and item[field] for field in required)

    def test_valid_item(self):
        item = {"headline": "Breaking News", "summary": "Something happened"}
        assert self._validate(item) is True

    def test_missing_headline(self):
        item = {"summary": "Something happened"}
        assert self._validate(item) is False

    def test_missing_summary(self):
        item = {"headline": "Breaking News"}
        assert self._validate(item) is False

    def test_empty_headline(self):
        item = {"headline": "", "summary": "Something"}
        assert self._validate(item) is False

    def test_empty_summary(self):
        item = {"headline": "News", "summary": ""}
        assert self._validate(item) is False

    def test_both_empty(self):
        item = {"headline": "", "summary": ""}
        assert self._validate(item) is False

    def test_both_missing(self):
        assert self._validate({}) is False

    def test_extra_fields_ok(self):
        item = {
            "headline": "News",
            "summary": "Details",
            "category": "Tech",
            "impact_score": 8,
        }
        assert self._validate(item) is True

    def test_none_headline(self):
        item = {"headline": None, "summary": "Details"}
        assert self._validate(item) is False

    def test_none_summary(self):
        item = {"headline": "News", "summary": None}
        assert self._validate(item) is False

    def test_whitespace_headline_is_valid(self):
        item = {"headline": "  ", "summary": "Details"}
        assert self._validate(item) is True  # non-empty string

    def test_zero_headline_is_falsy(self):
        item = {"headline": 0, "summary": "Details"}
        assert self._validate(item) is False


# --- _count_categories ---


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

    def _count(self, items):
        counts = {}
        for item in items:
            cat = item.get("category", "Other")
            counts[cat] = counts.get(cat, 0) + 1
        return counts

    def test_empty_items(self):
        assert self._count([]) == {}

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

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

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

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

    def test_mixed_with_and_without_category(self):
        items = [{"category": "Tech"}, {"headline": "No cat"}]
        result = self._count(items)
        assert result == {"Tech": 1, "Other": 1}

    def test_many_categories(self):
        items = [{"category": f"Cat{i}"} for i in range(10)]
        result = self._count(items)
        assert len(result) == 10
        assert all(v == 1 for v in result.values())

    def test_empty_string_category(self):
        items = [{"category": ""}]
        result = self._count(items)
        # Empty string is falsy but .get returns it, not "Other"
        assert result == {"": 1}


# --- _summarize_impact ---


class TestSummarizeImpact:
    """Tests for impact score summarization."""

    def _summarize(self, items):
        if not items:
            return {"average": 0, "high_impact_count": 0}
        scores = [item.get("impact_score", 5) for item in items]
        return {
            "average": sum(scores) / len(scores),
            "high_impact_count": len([s for s in scores if s >= 8]),
            "max": max(scores),
            "min": min(scores),
        }

    def test_empty_items(self):
        result = self._summarize([])
        assert result["average"] == 0
        assert result["high_impact_count"] == 0

    def test_single_item(self):
        result = self._summarize([{"impact_score": 7}])
        assert result["average"] == 7
        assert result["high_impact_count"] == 0
        assert result["max"] == 7
        assert result["min"] == 7

    def test_high_impact(self):
        items = [{"impact_score": 9}, {"impact_score": 8}]
        result = self._summarize(items)
        assert result["high_impact_count"] == 2

    def test_mixed_impact(self):
        items = [{"impact_score": 9}, {"impact_score": 3}]
        result = self._summarize(items)
        assert result["average"] == 6.0
        assert result["high_impact_count"] == 1
        assert result["max"] == 9
        assert result["min"] == 3

    def test_default_score_when_missing(self):
        items = [{"headline": "No score"}]
        result = self._summarize(items)
        assert result["average"] == 5  # default

    def test_all_high_impact(self):
        items = [{"impact_score": 10}, {"impact_score": 8}, {"impact_score": 9}]
        result = self._summarize(items)
        assert result["high_impact_count"] == 3

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

    def test_boundary_impact_8(self):
        items = [{"impact_score": 8}]
        result = self._summarize(items)
        assert result["high_impact_count"] == 1

    def test_boundary_impact_7(self):
        items = [{"impact_score": 7}]
        result = self._summarize(items)
        assert result["high_impact_count"] == 0

    def test_average_precision(self):
        items = [{"impact_score": 1}, {"impact_score": 2}, {"impact_score": 3}]
        result = self._summarize(items)
        assert result["average"] == 2.0


# --- _empty_analysis ---


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

    def _empty(self):
        return {
            "items": [],
            "item_count": 0,
            "big_picture": "",
            "watch_for": [],
            "patterns": "",
            "topics": [],
            "categories": {},
            "impact_summary": {"average": 0, "high_impact_count": 0},
            "timestamp": datetime.now(timezone.utc).isoformat(),
        }

    def test_items_empty_list(self):
        result = self._empty()
        assert result["items"] == []

    def test_item_count_zero(self):
        result = self._empty()
        assert result["item_count"] == 0

    def test_big_picture_empty(self):
        result = self._empty()
        assert result["big_picture"] == ""

    def test_watch_for_empty(self):
        result = self._empty()
        assert result["watch_for"] == []

    def test_patterns_empty(self):
        result = self._empty()
        assert result["patterns"] == ""

    def test_topics_empty(self):
        result = self._empty()
        assert result["topics"] == []

    def test_categories_empty(self):
        result = self._empty()
        assert result["categories"] == {}

    def test_impact_summary_zeroed(self):
        result = self._empty()
        assert result["impact_summary"]["average"] == 0
        assert result["impact_summary"]["high_impact_count"] == 0

    def test_has_timestamp(self):
        result = self._empty()
        assert "timestamp" in result
        assert len(result["timestamp"]) > 10

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


# --- _prepare_snippets ---


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

    def _prepare(self, results):
        snippets = []
        for i, result in enumerate(results):
            snippet = f"[{i + 1}] "
            if result.get("title"):
                snippet += f"Title: {result['title']}\n"
            if result.get("url"):
                snippet += f"URL: {result['url']}\n"
            if result.get("snippet"):
                snippet += f"Snippet: {result['snippet'][:200]}...\n"
            elif result.get("content"):
                snippet += f"Content: {result['content'][:200]}...\n"
            snippets.append(snippet)
        return "\n".join(snippets)

    def test_empty_results(self):
        assert self._prepare([]) == ""

    def test_with_title(self):
        results = [{"title": "Big News"}]
        output = self._prepare(results)
        assert "Title: Big News" in output

    def test_with_url(self):
        results = [{"url": "https://example.com"}]
        output = self._prepare(results)
        assert "URL: https://example.com" in output

    def test_with_snippet(self):
        results = [{"snippet": "Some content here"}]
        output = self._prepare(results)
        assert "Snippet: Some content here" in output

    def test_content_fallback_when_no_snippet(self):
        results = [{"content": "Full article text"}]
        output = self._prepare(results)
        assert "Content: Full article text" in output

    def test_snippet_preferred_over_content(self):
        results = [{"snippet": "Short", "content": "Long content"}]
        output = self._prepare(results)
        assert "Snippet: Short" in output
        assert "Content:" not in output

    def test_numbering(self):
        results = [{"title": "A"}, {"title": "B"}, {"title": "C"}]
        output = self._prepare(results)
        assert "[1]" in output
        assert "[2]" in output
        assert "[3]" in output

    def test_snippet_truncation_at_200(self):
        results = [{"snippet": "X" * 500}]
        output = self._prepare(results)
        # Should contain truncated snippet plus "..."
        assert "X" * 200 in output
        assert "X" * 201 not in output

    def test_content_truncation_at_200(self):
        results = [{"content": "Y" * 500}]
        output = self._prepare(results)
        assert "Y" * 200 in output
        assert "Y" * 201 not in output

    def test_all_fields(self):
        results = [
            {
                "title": "Title",
                "url": "https://x.com",
                "snippet": "Snippet text",
            }
        ]
        output = self._prepare(results)
        assert "Title: Title" in output
        assert "URL: https://x.com" in output
        assert "Snippet: Snippet text" in output

    def test_missing_all_fields(self):
        results = [{}]
        output = self._prepare(results)
        assert output.strip() == "[1]"


# --- Watch for bullet parsing ---


class TestWatchForBulletParsing:
    """Tests for parsing LLM watch-for bullet points."""

    def _parse_bullets(self, content):
        lines = content.strip().split("\n")
        watch_items = []
        for line in lines:
            line = line.strip()
            if line and line not in ["WATCH FOR:", "Watch for:"]:
                line = line.lstrip("-•* ")
                if line:
                    watch_items.append(line)
        return watch_items[:5]

    def test_standard_bullets(self):
        content = "- Item 1\n- Item 2\n- Item 3"
        result = self._parse_bullets(content)
        assert result == ["Item 1", "Item 2", "Item 3"]

    def test_removes_header(self):
        content = "WATCH FOR:\n- Item 1\n- Item 2"
        result = self._parse_bullets(content)
        assert "WATCH FOR:" not in result
        assert len(result) == 2

    def test_removes_lowercase_header(self):
        content = "Watch for:\n- Item 1"
        result = self._parse_bullets(content)
        assert "Watch for:" not in result

    def test_asterisk_bullets(self):
        content = "* Item 1\n* Item 2"
        result = self._parse_bullets(content)
        assert result == ["Item 1", "Item 2"]

    def test_dot_bullets(self):
        content = "• Point 1\n• Point 2"
        result = self._parse_bullets(content)
        assert result == ["Point 1", "Point 2"]

    def test_max_5_items(self):
        content = "\n".join(f"- Item {i}" for i in range(10))
        result = self._parse_bullets(content)
        assert len(result) == 5

    def test_skips_empty_lines(self):
        content = "- Item 1\n\n- Item 2\n\n"
        result = self._parse_bullets(content)
        assert result == ["Item 1", "Item 2"]

    def test_strips_whitespace(self):
        content = "  - Item 1  \n  - Item 2  "
        result = self._parse_bullets(content)
        assert result == ["Item 1", "Item 2"]

    def test_no_bullet_markers(self):
        content = "Plain text line 1\nPlain text line 2"
        result = self._parse_bullets(content)
        assert result == ["Plain text line 1", "Plain text line 2"]

    def test_empty_content(self):
        result = self._parse_bullets("")
        assert result == []


# --- Developing story filter ---


class TestDevelopingStoryFilter:
    """Tests for developing story filtering logic in generate_watch_for."""

    def _filter_developing(self, items):
        developing = [
            item for item in items if item.get("is_developing", False)
        ]
        if not developing:
            developing = items[:5]
        return developing

    def test_filters_developing_true(self):
        items = [
            {"headline": "A", "is_developing": True},
            {"headline": "B", "is_developing": False},
            {"headline": "C", "is_developing": True},
        ]
        result = self._filter_developing(items)
        assert len(result) == 2
        assert all(r["is_developing"] for r in result)

    def test_fallback_to_first_5_when_none_developing(self):
        items = [
            {"headline": f"News {i}", "is_developing": False} for i in range(10)
        ]
        result = self._filter_developing(items)
        assert len(result) == 5

    def test_empty_items(self):
        result = self._filter_developing([])
        assert result == []

    def test_all_developing(self):
        items = [{"headline": f"N{i}", "is_developing": True} for i in range(3)]
        result = self._filter_developing(items)
        assert len(result) == 3

    def test_missing_is_developing_key(self):
        items = [{"headline": "A"}, {"headline": "B"}]
        result = self._filter_developing(items)
        # No is_developing key -> defaults to False -> fallback to first 5
        assert len(result) == 2  # less than 5, returns all


# --- Category grouping for patterns ---


class TestCategoryGrouping:
    """Tests for category grouping used in generate_patterns."""

    def _group_by_category(self, items):
        by_category = {}
        for item in items:
            cat = item.get("category", "Other")
            if cat not in by_category:
                by_category[cat] = []
            by_category[cat].append(item["headline"])
        return by_category

    def test_empty(self):
        assert self._group_by_category([]) == {}

    def test_single_category(self):
        items = [
            {"headline": "A", "category": "Tech"},
            {"headline": "B", "category": "Tech"},
        ]
        result = self._group_by_category(items)
        assert result == {"Tech": ["A", "B"]}

    def test_multiple_categories(self):
        items = [
            {"headline": "A", "category": "Tech"},
            {"headline": "B", "category": "Science"},
        ]
        result = self._group_by_category(items)
        assert "Tech" in result
        assert "Science" in result

    def test_default_category(self):
        items = [{"headline": "A"}]
        result = self._group_by_category(items)
        assert result == {"Other": ["A"]}

    def test_category_summary_format(self):
        items = [
            {"headline": "A", "category": "Tech"},
            {"headline": "B", "category": "Tech"},
            {"headline": "C", "category": "Science"},
        ]
        by_category = self._group_by_category(items)
        summary = "\n".join(
            f"{cat}: {len(headlines)} stories"
            for cat, headlines in by_category.items()
        )
        assert "Tech: 2 stories" in summary
        assert "Science: 1 stories" in summary


# --- Topic deduplication logic ---


class TestTopicDeduplication:
    """Tests for extract_topics deduplication logic."""

    def _dedup_topics(self, topics_list):
        topic_counts = {}
        topic_metadata = {}
        for info in topics_list:
            name = info["name"]
            if name not in topic_counts:
                topic_counts[name] = 0
                topic_metadata[name] = info
            topic_counts[name] += 1
            if info["impact_score"] > topic_metadata[name]["impact_score"]:
                topic_metadata[name] = info

        final = []
        for topic, count in sorted(
            topic_counts.items(), key=lambda x: x[1], reverse=True
        ):
            metadata = topic_metadata[topic]
            metadata["frequency"] = count
            metadata["query"] = f"{topic} latest developments news"
            final.append(metadata)
        return final[:10]

    def test_single_topic(self):
        topics = [{"name": "AI", "impact_score": 7}]
        result = self._dedup_topics(topics)
        assert len(result) == 1
        assert result[0]["frequency"] == 1

    def test_deduplicates_same_name(self):
        topics = [
            {"name": "AI", "impact_score": 5},
            {"name": "AI", "impact_score": 8},
        ]
        result = self._dedup_topics(topics)
        assert len(result) == 1
        assert result[0]["frequency"] == 2

    def test_keeps_highest_impact(self):
        topics = [
            {"name": "AI", "impact_score": 3},
            {"name": "AI", "impact_score": 9},
        ]
        result = self._dedup_topics(topics)
        assert result[0]["impact_score"] == 9

    def test_sorts_by_frequency(self):
        topics = [
            {"name": "AI", "impact_score": 5},
            {"name": "AI", "impact_score": 5},
            {"name": "Climate", "impact_score": 5},
        ]
        result = self._dedup_topics(topics)
        assert result[0]["name"] == "AI"

    def test_max_10_topics(self):
        topics = [{"name": f"Topic{i}", "impact_score": 5} for i in range(15)]
        result = self._dedup_topics(topics)
        assert len(result) == 10

    def test_generates_query_field(self):
        topics = [{"name": "climate", "impact_score": 7}]
        result = self._dedup_topics(topics)
        assert result[0]["query"] == "climate latest developments news"

    def test_empty_topics(self):
        result = self._dedup_topics([])
        assert result == []

    def test_multiple_unique_topics(self):
        topics = [
            {"name": "AI", "impact_score": 5},
            {"name": "Climate", "impact_score": 7},
            {"name": "Economy", "impact_score": 3},
        ]
        result = self._dedup_topics(topics)
        assert len(result) == 3


# --- analyze_news components structure ---


class TestAnalyzeNewsComponentsStructure:
    """Tests for analyze_news return structure when items exist."""

    def _build_components(self, news_items, search_results):
        components = {
            "items": news_items,
            "item_count": len(news_items),
            "search_result_count": len(search_results),
            "timestamp": datetime.now(timezone.utc).isoformat(),
        }
        return components

    def test_item_count_matches(self):
        items = [{"headline": "A"}, {"headline": "B"}]
        components = self._build_components(items, items)
        assert components["item_count"] == 2

    def test_search_result_count(self):
        components = self._build_components([], [1, 2, 3])
        assert components["search_result_count"] == 3

    def test_has_timestamp(self):
        components = self._build_components([], [])
        assert "timestamp" in components

    def test_items_preserved(self):
        items = [{"headline": "Test"}]
        components = self._build_components(items, items)
        assert components["items"] == items

    def test_empty_items_zero_count(self):
        components = self._build_components([], [])
        assert components["item_count"] == 0


# --- Summary truncation for LLM prompts ---


class TestSummaryTruncation:
    """Tests for summary truncation used in big_picture/watch_for prompts."""

    def _prepare_summaries(self, items, max_items=10):
        return "\n".join(
            f"- {item['headline']}: {item.get('summary', '')[:100]}..."
            for item in items[:max_items]
        )

    def test_basic_summary(self):
        items = [
            {"headline": "AI News", "summary": "Something happened with AI"}
        ]
        result = self._prepare_summaries(items)
        assert "AI News: Something happened with AI..." in result

    def test_truncates_at_100_chars(self):
        items = [{"headline": "News", "summary": "A" * 200}]
        result = self._prepare_summaries(items)
        assert "A" * 100 in result
        assert "A" * 101 not in result

    def test_max_10_items(self):
        items = [{"headline": f"H{i}", "summary": f"S{i}"} for i in range(20)]
        result = self._prepare_summaries(items)
        assert "H9" in result
        assert "H10" not in result

    def test_missing_summary(self):
        items = [{"headline": "News"}]
        result = self._prepare_summaries(items)
        assert "News: ..." in result

    def test_empty_items(self):
        result = self._prepare_summaries([])
        assert result == ""
