"""
Deep behavioral tests for news query detection heuristics.
Tests the is_news_query and has_news_metadata logic from api.py,
metadata parsing patterns, and content processing logic.
"""

import json
import re


# --- has_news_metadata detection ---


class TestHasNewsMetadata:
    """Tests for the has_news_metadata detection heuristic."""

    def _has_news_metadata(self, metadata):
        """Reproduce the has_news_metadata check from api.py:212-215."""
        return (
            metadata.get("generated_headline") is not None
            or metadata.get("generated_topics") is not None
        )

    def test_headline_present(self):
        assert self._has_news_metadata(
            {"generated_headline": "AI Breakthrough"}
        )

    def test_topics_present(self):
        assert self._has_news_metadata({"generated_topics": ["AI", "Climate"]})

    def test_both_present(self):
        assert self._has_news_metadata(
            {"generated_headline": "Test", "generated_topics": ["AI"]}
        )

    def test_neither_present(self):
        assert not self._has_news_metadata({})

    def test_empty_headline_is_truthy(self):
        """Empty string is not None, so it counts as present."""
        assert self._has_news_metadata({"generated_headline": ""})

    def test_empty_topics_list_is_truthy(self):
        """Empty list is not None, so it counts as present."""
        assert self._has_news_metadata({"generated_topics": []})

    def test_none_headline_is_falsy(self):
        assert not self._has_news_metadata({"generated_headline": None})

    def test_none_topics_is_falsy(self):
        assert not self._has_news_metadata({"generated_topics": None})

    def test_unrelated_keys_ignored(self):
        assert not self._has_news_metadata({"title": "Test", "other": "data"})

    def test_headline_none_topics_present(self):
        assert self._has_news_metadata(
            {"generated_headline": None, "generated_topics": ["AI"]}
        )


# --- is_news_query detection ---


class TestIsNewsQuery:
    """Tests for the is_news_query detection heuristic."""

    def _is_news_query(self, query, metadata, has_news_metadata=False):
        """Reproduce the is_news_query check from api.py:218-232."""
        query_lower = query.lower()
        return (
            has_news_metadata
            or metadata.get("is_news_search")
            or metadata.get("search_type") == "news_analysis"
            or "breaking news" in query_lower
            or "news stories" in query_lower
            or (
                "today" in query_lower
                and ("news" in query_lower or "breaking" in query_lower)
            )
            or "latest news" in query_lower
        )

    def test_has_news_metadata_makes_news(self):
        assert self._is_news_query("anything", {}, has_news_metadata=True)

    def test_is_news_search_flag(self):
        assert self._is_news_query("test", {"is_news_search": True})

    def test_search_type_news_analysis(self):
        assert self._is_news_query("test", {"search_type": "news_analysis"})

    def test_breaking_news_in_query(self):
        assert self._is_news_query("What is the breaking news about AI?", {})

    def test_news_stories_in_query(self):
        assert self._is_news_query("Latest news stories about climate", {})

    def test_today_and_news_in_query(self):
        assert self._is_news_query("What news happened today?", {})

    def test_today_and_breaking_in_query(self):
        assert self._is_news_query("What is breaking today?", {})

    def test_latest_news_in_query(self):
        assert self._is_news_query("latest news about tech", {})

    def test_regular_query_not_news(self):
        assert not self._is_news_query("How to cook pasta", {})

    def test_today_alone_not_news(self):
        assert not self._is_news_query("What happened today?", {})

    def test_news_alone_not_news(self):
        """Just 'news' in query without matching patterns is not detected."""
        assert not self._is_news_query("I read some news yesterday", {})

    def test_case_insensitive_breaking(self):
        assert self._is_news_query("BREAKING NEWS about AI", {})

    def test_case_insensitive_latest(self):
        assert self._is_news_query("LATEST NEWS update", {})

    def test_search_type_not_news_analysis(self):
        assert not self._is_news_query("test", {"search_type": "general"})

    def test_is_news_search_false(self):
        assert not self._is_news_query("test", {"is_news_search": False})

    def test_is_news_search_none(self):
        assert not self._is_news_query("test", {"is_news_search": None})


# --- Metadata parsing ---


class TestMetadataParsing:
    """Tests for the JSON metadata parsing pattern from api.py:198-208."""

    def _parse_metadata(self, research_meta):
        """Reproduce metadata parsing from api.py."""
        metadata = {}
        if research_meta:
            try:
                if isinstance(research_meta, dict):
                    metadata = research_meta
                else:
                    metadata = json.loads(research_meta)
            except (json.JSONDecodeError, TypeError):
                metadata = {}
        return metadata

    def test_dict_input(self):
        result = self._parse_metadata({"key": "val"})
        assert result == {"key": "val"}

    def test_json_string_input(self):
        result = self._parse_metadata('{"key": "val"}')
        assert result == {"key": "val"}

    def test_none_input(self):
        result = self._parse_metadata(None)
        assert result == {}

    def test_empty_string(self):
        result = self._parse_metadata("")
        assert result == {}

    def test_invalid_json(self):
        result = self._parse_metadata("not json")
        assert result == {}

    def test_empty_dict(self):
        result = self._parse_metadata({})
        assert result == {}

    def test_complex_json(self):
        data = {"generated_headline": "Test", "generated_topics": ["AI"]}
        result = self._parse_metadata(json.dumps(data))
        assert result["generated_headline"] == "Test"

    def test_nested_dict(self):
        result = self._parse_metadata({"nested": {"deep": True}})
        assert result["nested"]["deep"] is True

    def test_dict_not_re_parsed(self):
        """Dict input should be used directly, not serialized/deserialized."""
        original = {"key": "val"}
        result = self._parse_metadata(original)
        assert result is original

    def test_integer_input(self):
        result = self._parse_metadata(42)
        assert result == {}

    def test_list_input(self):
        result = self._parse_metadata([1, 2, 3])
        # json.loads on a list string works, but isinstance check fails
        # and json.loads(list) raises TypeError
        assert result == {}


# --- Link extraction from content ---


class TestLinkExtraction:
    """Tests for link extraction logic from api.py:332-379."""

    def _extract_links(self, content, max_links=3):
        """Reproduce link extraction from api.py."""
        links = []
        if not content:
            return links
        try:
            report_lines = content.split("\n")
            link_count = 0
            for i, line in enumerate(report_lines[:100]):
                if "URL:" in line:
                    url = line.split("URL:", 1)[1].strip()
                    if url.startswith("http"):
                        title = ""
                        if i > 0:
                            title_line = report_lines[i - 1].strip()
                            title = re.sub(
                                r"^\[[^\]]+\]\s*", "", title_line
                            ).strip()
                        if not title:
                            domain = url.split("//")[-1].split("/")[0]
                            title = domain.replace("www.", "")
                        links.append(
                            {
                                "url": url,
                                "title": title[:50] + "..."
                                if len(title) > 50
                                else title,
                            }
                        )
                        link_count += 1
                        if link_count >= max_links:
                            break
        except Exception:
            pass
        return links

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

    def test_none_content(self):
        assert self._extract_links(None) == []

    def test_no_links_in_content(self):
        content = "This is just regular text.\nNo links here."
        assert self._extract_links(content) == []

    def test_single_link(self):
        content = "Article Title\nURL: https://example.com/article"
        links = self._extract_links(content)
        assert len(links) == 1
        assert links[0]["url"] == "https://example.com/article"

    def test_title_from_previous_line(self):
        content = "Important Article\nURL: https://example.com/article"
        links = self._extract_links(content)
        assert links[0]["title"] == "Important Article"

    def test_max_three_links(self):
        lines = []
        for i in range(10):
            lines.append(f"Title {i}")
            lines.append(f"URL: https://example.com/{i}")
        content = "\n".join(lines)
        links = self._extract_links(content)
        assert len(links) == 3

    def test_non_http_url_ignored(self):
        content = "Title\nURL: ftp://example.com/file"
        links = self._extract_links(content)
        assert len(links) == 0

    def test_strips_citation_numbers(self):
        content = "[12, 26] Article Title\nURL: https://example.com/article"
        links = self._extract_links(content)
        assert links[0]["title"] == "Article Title"

    def test_domain_fallback_when_no_title(self):
        content = "URL: https://reuters.com/article/test"
        links = self._extract_links(content)
        assert links[0]["title"] == "reuters.com"

    def test_www_stripped_from_domain(self):
        content = "URL: https://www.bbc.com/news/article"
        links = self._extract_links(content)
        assert links[0]["title"] == "bbc.com"

    def test_title_truncated_at_50(self):
        long_title = "A" * 60
        content = f"{long_title}\nURL: https://example.com/article"
        links = self._extract_links(content)
        assert len(links[0]["title"]) == 53  # 50 + "..."
        assert links[0]["title"].endswith("...")

    def test_title_not_truncated_if_short(self):
        content = "Short Title\nURL: https://example.com/article"
        links = self._extract_links(content)
        assert links[0]["title"] == "Short Title"
        assert "..." not in links[0]["title"]

    def test_url_with_query_params(self):
        content = "Title\nURL: https://example.com/article?id=123&lang=en"
        links = self._extract_links(content)
        assert links[0]["url"] == "https://example.com/article?id=123&lang=en"

    def test_only_first_100_lines_checked(self):
        lines = ["Regular line"] * 100
        lines.append("Title")
        lines.append("URL: https://example.com/hidden")
        content = "\n".join(lines)
        links = self._extract_links(content)
        assert len(links) == 0

    def test_multiple_urls_on_same_line_takes_first(self):
        content = "Title\nURL: https://first.com URL: https://second.com"
        links = self._extract_links(content)
        # split("URL:", 1) means only first URL is captured
        assert "first.com" in links[0]["url"]

    def test_empty_url_after_prefix(self):
        content = "Title\nURL: "
        links = self._extract_links(content)
        assert len(links) == 0  # Empty string doesn't start with http


# --- Content summary extraction ---


class TestContentSummaryExtraction:
    """Tests for summary extraction from content lines (api.py:263-269)."""

    def _extract_summary(self, content):
        """Reproduce summary extraction from api.py."""
        if not content:
            return ""
        lines = content.split("\n")
        for line in lines:
            if line.strip() and not line.startswith("#"):
                return line.strip()
        return ""

    def test_first_non_empty_line(self):
        content = "\n\nThis is the summary.\nMore text."
        assert self._extract_summary(content) == "This is the summary."

    def test_skips_headers(self):
        content = "# Header\nThis is content."
        assert self._extract_summary(content) == "This is content."

    def test_skips_empty_lines(self):
        content = "\n\n\nFirst real line."
        assert self._extract_summary(content) == "First real line."

    def test_empty_content(self):
        assert self._extract_summary("") == ""

    def test_none_content(self):
        assert self._extract_summary(None) == ""

    def test_only_headers(self):
        content = "# Header 1\n# Header 2\n## Sub header"
        assert self._extract_summary(content) == ""

    def test_only_empty_lines(self):
        content = "\n\n\n"
        assert self._extract_summary(content) == ""

    def test_strips_whitespace(self):
        content = "   Summary with spaces   "
        assert self._extract_summary(content) == "Summary with spaces"


# --- Headline generation fallback ---


class TestHeadlineGenerationFallback:
    """Tests for headline generation fallback logic from api.py:280-294."""

    def _get_headline(self, row, metadata):
        """Reproduce headline generation from api.py."""
        headline = row.get("title") or metadata.get("generated_headline")
        if not headline and metadata.get("is_news_search"):
            subscription_name = metadata.get("subscription_name")
            if subscription_name:
                headline = f"News Update: {subscription_name}"
            else:
                headline = f"News: {row['query'][:60]}..."
        return headline

    def test_uses_title_first(self):
        result = self._get_headline(
            {"title": "DB Title", "query": "test"},
            {"generated_headline": "Meta Headline"},
        )
        assert result == "DB Title"

    def test_uses_metadata_headline_if_no_title(self):
        result = self._get_headline(
            {"title": None, "query": "test"},
            {"generated_headline": "Meta Headline"},
        )
        assert result == "Meta Headline"

    def test_subscription_name_fallback(self):
        result = self._get_headline(
            {"title": None, "query": "test"},
            {"is_news_search": True, "subscription_name": "My Feed"},
        )
        assert result == "News Update: My Feed"

    def test_query_fallback(self):
        result = self._get_headline(
            {"title": None, "query": "What's happening in AI today?"},
            {"is_news_search": True},
        )
        assert result.startswith("News: ")
        assert "AI" in result

    def test_query_truncated_at_60(self):
        long_query = "A" * 100
        result = self._get_headline(
            {"title": None, "query": long_query},
            {"is_news_search": True},
        )
        # "News: " + 60 chars + "..."
        assert len(result) == len("News: ") + 60 + len("...")

    def test_no_fallback_for_non_news(self):
        result = self._get_headline(
            {"title": None, "query": "test"},
            {"is_news_search": False},
        )
        assert result is None

    def test_empty_title_falls_through(self):
        """Empty string is falsy, should fall through."""
        result = self._get_headline(
            {"title": "", "query": "test"},
            {"generated_headline": "Headline"},
        )
        assert result == "Headline"

    def test_both_none_no_news_returns_none(self):
        result = self._get_headline(
            {"title": None, "query": "test"},
            {},
        )
        assert result is None


# --- Category and topics defaults ---


class TestCategoryTopicsDefaults:
    """Tests for category and topics default logic from api.py:324-330."""

    def test_category_from_metadata(self):
        metadata = {"category": "Technology"}
        category = metadata.get("category")
        if not category:
            category = "[Uncategorized]"
        assert category == "Technology"

    def test_category_default(self):
        metadata = {}
        category = metadata.get("category")
        if not category:
            category = "[Uncategorized]"
        assert category == "[Uncategorized]"

    def test_category_none_gets_default(self):
        metadata = {"category": None}
        category = metadata.get("category")
        if not category:
            category = "[Uncategorized]"
        assert category == "[Uncategorized]"

    def test_topics_from_metadata(self):
        metadata = {"generated_topics": ["AI", "Climate"]}
        topics = metadata.get("generated_topics")
        if not topics:
            topics = ["[No topics]"]
        assert topics == ["AI", "Climate"]

    def test_topics_default(self):
        metadata = {}
        topics = metadata.get("generated_topics")
        if not topics:
            topics = ["[No topics]"]
        assert topics == ["[No topics]"]

    def test_topics_empty_list_gets_default(self):
        metadata = {"generated_topics": []}
        topics = metadata.get("generated_topics")
        if not topics:
            topics = ["[No topics]"]
        assert topics == ["[No topics]"]


# --- Source determination ---


class TestSourceDetermination:
    """Tests for source determination logic from api.py:461-465."""

    def test_news_items_source(self):
        news_items = [{"is_news": True}, {"is_news": False}]
        source = (
            "news_items"
            if any(item.get("is_news", False) for item in news_items)
            else "research_history"
        )
        assert source == "news_items"

    def test_research_history_source(self):
        news_items = [{"is_news": False}, {"is_news": False}]
        source = (
            "news_items"
            if any(item.get("is_news", False) for item in news_items)
            else "research_history"
        )
        assert source == "research_history"

    def test_empty_items_source(self):
        news_items = []
        source = (
            "news_items"
            if any(item.get("is_news", False) for item in news_items)
            else "research_history"
        )
        assert source == "research_history"

    def test_all_news_items(self):
        news_items = [{"is_news": True}, {"is_news": True}]
        source = (
            "news_items"
            if any(item.get("is_news", False) for item in news_items)
            else "research_history"
        )
        assert source == "news_items"

    def test_missing_is_news_key(self):
        news_items = [{"title": "Test"}]
        source = (
            "news_items"
            if any(item.get("is_news", False) for item in news_items)
            else "research_history"
        )
        assert source == "research_history"


# --- Subscription filter pattern ---


class TestSubscriptionFilterPattern:
    """Tests for subscription_id filtering from api.py:139-145."""

    def test_all_filter_not_applied(self):
        subscription_id = "all"
        should_filter = subscription_id and subscription_id != "all"
        assert not should_filter

    def test_specific_id_filter_applied(self):
        subscription_id = "sub-123"
        should_filter = subscription_id and subscription_id != "all"
        assert should_filter

    def test_none_filter_not_applied(self):
        subscription_id = None
        should_filter = subscription_id and subscription_id != "all"
        assert not should_filter

    def test_empty_string_filter_not_applied(self):
        subscription_id = ""
        should_filter = subscription_id and subscription_id != "all"
        assert not should_filter


# --- Research ID resolution ---


class TestResearchIdResolution:
    """Tests for research ID resolution from api.py:321."""

    def test_uuid_id_preferred(self):
        row = {"uuid_id": "uuid-123", "id": 42}
        research_id = row.get("uuid_id") or str(row["id"])
        assert research_id == "uuid-123"

    def test_falls_back_to_id(self):
        row = {"uuid_id": None, "id": 42}
        research_id = row.get("uuid_id") or str(row["id"])
        assert research_id == "42"

    def test_id_converted_to_string(self):
        row = {"uuid_id": None, "id": 99}
        research_id = row.get("uuid_id") or str(row["id"])
        assert isinstance(research_id, str)

    def test_empty_uuid_falls_back(self):
        row = {"uuid_id": "", "id": 55}
        research_id = row.get("uuid_id") or str(row["id"])
        assert research_id == "55"


# --- Status filtering ---


class TestStatusFiltering:
    """Tests for status filtering from api.py:307-311."""

    def test_in_progress_filtered(self):
        status = "in_progress"
        should_skip = status in ["in_progress", "suspended"]
        assert should_skip

    def test_suspended_filtered(self):
        status = "suspended"
        should_skip = status in ["in_progress", "suspended"]
        assert should_skip

    def test_completed_not_filtered(self):
        status = "completed"
        should_skip = status in ["in_progress", "suspended"]
        assert not should_skip

    def test_failed_not_filtered(self):
        status = "failed"
        should_skip = status in ["in_progress", "suspended"]
        assert not should_skip

    def test_active_not_filtered(self):
        status = "active"
        should_skip = status in ["in_progress", "suspended"]
        assert not should_skip
