"""
Deep behavioral tests for BaseCard helper methods, CardSource, CardVersion, and card types.
Tests headline extraction, summary extraction, impact calculation, topic extraction,
entity extraction, to_base_dict, and dataclass behavior.
"""

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

import pytest

from local_deep_research.news.core.base_card import (
    BaseCard,
    CardSource,
    CardVersion,
    NewsCard,
    ResearchCard,
    UpdateCard,
    OverviewCard,
)
from local_deep_research.news.core.utils import generate_card_id


def _make_source(**kwargs):
    """Create a CardSource with defaults."""
    defaults = {"type": "test", "source_id": "s1", "created_from": "test"}
    defaults.update(kwargs)
    return CardSource(**defaults)


def _make_news_card(**kwargs):
    """Create a NewsCard with defaults."""
    defaults = {
        "topic": "Test Topic",
        "source": _make_source(),
        "user_id": "u1",
    }
    defaults.update(kwargs)
    return NewsCard(**defaults)


# --- CardSource ---


class TestCardSourceDataclass:
    """Tests for CardSource dataclass."""

    def test_type_field(self):
        cs = CardSource(type="news_item")
        assert cs.type == "news_item"

    def test_source_id_default_none(self):
        cs = CardSource(type="test")
        assert cs.source_id is None

    def test_source_id_custom(self):
        cs = CardSource(type="test", source_id="s-123")
        assert cs.source_id == "s-123"

    def test_created_from_default_empty(self):
        cs = CardSource(type="test")
        assert cs.created_from == ""

    def test_created_from_custom(self):
        cs = CardSource(type="test", created_from="Analysis")
        assert cs.created_from == "Analysis"

    def test_metadata_default_empty_dict(self):
        cs = CardSource(type="test")
        assert cs.metadata == {}

    def test_metadata_custom(self):
        cs = CardSource(type="test", metadata={"key": "val"})
        assert cs.metadata == {"key": "val"}

    def test_metadata_not_shared_between_instances(self):
        cs1 = CardSource(type="a")
        cs2 = CardSource(type="b")
        cs1.metadata["x"] = 1
        assert "x" not in cs2.metadata

    def test_valid_source_types(self):
        """Source type accepts any string."""
        for t in ["news_item", "user_search", "subscription", "news_research"]:
            cs = CardSource(type=t)
            assert cs.type == t


# --- CardVersion ---


class TestCardVersionDataclass:
    """Tests for CardVersion dataclass."""

    def test_version_id_stored(self):
        cv = CardVersion(
            version_id="v1",
            created_at=datetime.now(timezone.utc),
            content={"data": "test"},
            query_used="test query",
        )
        assert cv.version_id == "v1"

    def test_auto_generates_id_when_empty(self):
        cv = CardVersion(
            version_id="",
            created_at=datetime.now(timezone.utc),
            content={},
            query_used="q",
        )
        assert cv.version_id != ""
        assert len(cv.version_id) > 0

    def test_keeps_provided_id(self):
        cv = CardVersion(
            version_id="my-custom-id",
            created_at=datetime.now(timezone.utc),
            content={},
            query_used="q",
        )
        assert cv.version_id == "my-custom-id"

    def test_search_strategy_default_none(self):
        cv = CardVersion(
            version_id="v1",
            created_at=datetime.now(timezone.utc),
            content={},
            query_used="q",
        )
        assert cv.search_strategy is None

    def test_search_strategy_custom(self):
        cv = CardVersion(
            version_id="v1",
            created_at=datetime.now(timezone.utc),
            content={},
            query_used="q",
            search_strategy="deep_analysis",
        )
        assert cv.search_strategy == "deep_analysis"

    def test_content_stored(self):
        cv = CardVersion(
            version_id="v1",
            created_at=datetime.now(timezone.utc),
            content={"findings": ["f1"]},
            query_used="q",
        )
        assert cv.content == {"findings": ["f1"]}


# --- _extract_headline ---


class TestExtractHeadline:
    """Tests for BaseCard._extract_headline."""

    def test_returns_headline_field(self):
        card = _make_news_card()
        result = card._extract_headline({"headline": "Breaking News"})
        assert result == "Breaking News"

    def test_falls_back_to_title(self):
        card = _make_news_card()
        result = card._extract_headline({"title": "Title Text"})
        assert result == "Title Text"

    def test_falls_back_to_query(self):
        card = _make_news_card()
        result = card._extract_headline({"query": "What happened today?"})
        assert result == "What happened today?"

    def test_query_truncated_to_100(self):
        card = _make_news_card()
        long_query = "x" * 200
        result = card._extract_headline({"query": long_query})
        assert len(result) == 100

    def test_headline_takes_priority_over_title(self):
        card = _make_news_card()
        result = card._extract_headline(
            {"headline": "Headline", "title": "Title"}
        )
        assert result == "Headline"

    def test_title_takes_priority_over_query(self):
        card = _make_news_card()
        result = card._extract_headline({"title": "Title", "query": "Query"})
        assert result == "Title"

    def test_empty_dict_returns_empty(self):
        card = _make_news_card()
        result = card._extract_headline({})
        assert result == ""

    def test_none_headline_falls_through(self):
        card = _make_news_card()
        result = card._extract_headline({"headline": None, "title": "Title"})
        assert result == "Title"

    def test_empty_headline_falls_through(self):
        card = _make_news_card()
        result = card._extract_headline({"headline": "", "title": "Title"})
        assert result == "Title"


# --- _extract_summary ---


class TestExtractSummary:
    """Tests for BaseCard._extract_summary."""

    def test_returns_summary_field(self):
        card = _make_news_card()
        result = card._extract_summary({"summary": "Brief summary"})
        assert result == "Brief summary"

    def test_falls_back_to_current_knowledge(self):
        card = _make_news_card()
        result = card._extract_summary(
            {"current_knowledge": "Current state of affairs"}
        )
        assert result == "Current state of affairs"

    def test_falls_back_to_formatted_findings(self):
        card = _make_news_card()
        result = card._extract_summary(
            {"formatted_findings": "Finding 1. Finding 2."}
        )
        assert result == "Finding 1. Finding 2."

    def test_formatted_findings_truncated_to_500(self):
        card = _make_news_card()
        long_findings = "x" * 1000
        result = card._extract_summary({"formatted_findings": long_findings})
        assert len(result) == 500

    def test_summary_takes_priority(self):
        card = _make_news_card()
        result = card._extract_summary(
            {"summary": "Summary", "current_knowledge": "Knowledge"}
        )
        assert result == "Summary"

    def test_empty_dict_returns_empty(self):
        card = _make_news_card()
        result = card._extract_summary({})
        assert result == ""


# --- _calculate_impact ---


class TestCalculateImpact:
    """Tests for BaseCard._calculate_impact."""

    def test_empty_result_gives_base_score(self):
        card = _make_news_card()
        result = card._calculate_impact({})
        assert result == 5

    def test_many_findings_increases_score(self):
        card = _make_news_card()
        result = card._calculate_impact(
            {"findings": [{"f": i} for i in range(20)]}
        )
        assert result > 5

    def test_many_sources_increases_score(self):
        card = _make_news_card()
        result = card._calculate_impact(
            {"sources": [{"s": i} for i in range(15)]}
        )
        assert result > 5

    def test_score_capped_at_10(self):
        card = _make_news_card()
        result = card._calculate_impact(
            {
                "findings": [{"f": i} for i in range(100)],
                "sources": [{"s": i} for i in range(100)],
            }
        )
        assert result == 10

    def test_score_minimum_is_1(self):
        card = _make_news_card()
        result = card._calculate_impact({})
        assert result >= 1

    def test_five_findings_gives_six(self):
        card = _make_news_card()
        result = card._calculate_impact(
            {"findings": [{"f": i} for i in range(5)]}
        )
        # 5 + (5 // 5) + (0 // 3) = 5 + 1 + 0 = 6
        assert result == 6

    def test_combined_findings_and_sources(self):
        card = _make_news_card()
        result = card._calculate_impact(
            {
                "findings": [{"f": i} for i in range(10)],
                "sources": [{"s": i} for i in range(9)],
            }
        )
        # 5 + (10 // 5) + (9 // 3) = 5 + 2 + 3 = 10
        assert result == 10


# --- _extract_topics ---


class TestExtractTopics:
    """Tests for BaseCard._extract_topics."""

    def test_returns_topics_field(self):
        card = _make_news_card()
        result = card._extract_topics({"topics": ["AI", "Climate"]})
        assert result == ["AI", "Climate"]

    def test_empty_topics_extracts_from_query(self):
        card = _make_news_card()
        result = card._extract_topics(
            {"topics": [], "query": "artificial intelligence breakthroughs"}
        )
        assert len(result) > 0

    def test_no_topics_extracts_from_query(self):
        card = _make_news_card()
        result = card._extract_topics(
            {"query": "artificial intelligence breakthroughs"}
        )
        assert "artificial" in result

    def test_short_words_excluded(self):
        card = _make_news_card()
        result = card._extract_topics({"query": "the big AI news today"})
        # Words <= 4 chars excluded: "the", "big", "AI", "news"
        assert "the" not in result
        assert "big" not in result

    def test_max_five_topics_from_query(self):
        card = _make_news_card()
        result = card._extract_topics(
            {
                "query": "artificial intelligence machine learning neural networks deep learning transformers"
            }
        )
        assert len(result) <= 5

    def test_existing_topics_not_overridden(self):
        card = _make_news_card()
        result = card._extract_topics(
            {"topics": ["Custom"], "query": "artificial intelligence"}
        )
        assert result == ["Custom"]


# --- _extract_entities ---


class TestExtractEntities:
    """Tests for BaseCard._extract_entities."""

    def test_returns_entities_field(self):
        card = _make_news_card()
        result = card._extract_entities(
            {
                "entities": {
                    "people": ["John"],
                    "places": [],
                    "organizations": [],
                }
            }
        )
        assert result["people"] == ["John"]

    def test_empty_result_has_default_keys(self):
        card = _make_news_card()
        result = card._extract_entities({})
        assert "people" in result
        assert "places" in result
        assert "organizations" in result

    def test_default_lists_are_empty(self):
        card = _make_news_card()
        result = card._extract_entities({})
        assert result["people"] == []
        assert result["places"] == []
        assert result["organizations"] == []


# --- get_latest_version ---


class TestGetLatestVersion:
    """Tests for BaseCard.get_latest_version."""

    def test_empty_versions_returns_none(self):
        card = _make_news_card()
        assert card.get_latest_version() is None

    def test_single_version(self):
        card = _make_news_card()
        now = datetime.now(timezone.utc)
        v = CardVersion(
            version_id="v1", created_at=now, content={}, query_used="q"
        )
        card.versions.append(v)
        assert card.get_latest_version() is v

    def test_returns_newest(self):
        card = _make_news_card()
        from datetime import timedelta

        old = CardVersion(
            version_id="v1",
            created_at=datetime.now(timezone.utc) - timedelta(hours=1),
            content={},
            query_used="q1",
        )
        new = CardVersion(
            version_id="v2",
            created_at=datetime.now(timezone.utc),
            content={},
            query_used="q2",
        )
        card.versions.extend([old, new])
        assert card.get_latest_version() is new


# --- to_base_dict ---


class TestToBaseDict:
    """Tests for BaseCard.to_base_dict."""

    def test_has_id(self):
        card = _make_news_card()
        d = card.to_base_dict()
        assert "id" in d

    def test_has_topic(self):
        card = _make_news_card(topic="My Topic")
        d = card.to_base_dict()
        assert d["topic"] == "My Topic"

    def test_has_user_id(self):
        card = _make_news_card(user_id="user-42")
        d = card.to_base_dict()
        assert d["user_id"] == "user-42"

    def test_has_created_at_iso(self):
        card = _make_news_card()
        d = card.to_base_dict()
        assert d["created_at"] is not None
        assert "T" in d["created_at"]

    def test_has_updated_at_iso(self):
        card = _make_news_card()
        d = card.to_base_dict()
        assert d["updated_at"] is not None

    def test_has_source_dict(self):
        card = _make_news_card()
        d = card.to_base_dict()
        assert "source" in d
        assert d["source"]["type"] == "test"

    def test_versions_count(self):
        card = _make_news_card()
        d = card.to_base_dict()
        assert d["versions_count"] == 0

    def test_has_interaction(self):
        card = _make_news_card()
        d = card.to_base_dict()
        assert d["interaction"]["votes_up"] == 0

    def test_has_card_type(self):
        card = _make_news_card()
        d = card.to_base_dict()
        assert d["card_type"] == "news"

    def test_has_metadata(self):
        card = _make_news_card(metadata={"key": "val"})
        d = card.to_base_dict()
        assert d["metadata"] == {"key": "val"}


# --- NewsCard specifics ---


class TestNewsCardBehavior:
    """Tests for NewsCard-specific behavior."""

    def test_card_type_is_news(self):
        card = _make_news_card()
        assert card.get_card_type() == "news"

    def test_headline_defaults_to_topic(self):
        card = _make_news_card(topic="My Topic")
        assert card.headline == "My Topic"

    def test_headline_custom(self):
        card = _make_news_card(headline="Custom Headline")
        assert card.headline == "Custom Headline"

    def test_default_category(self):
        card = _make_news_card()
        assert card.category == "General"

    def test_default_impact_score(self):
        card = _make_news_card()
        assert card.impact_score == 5

    def test_default_is_developing_false(self):
        card = _make_news_card()
        assert card.is_developing is False

    def test_to_dict_includes_headline(self):
        card = _make_news_card(headline="Test Headline")
        d = card.to_dict()
        assert d["headline"] == "Test Headline"

    def test_to_dict_includes_category(self):
        card = _make_news_card(category="Tech")
        d = card.to_dict()
        assert d["category"] == "Tech"

    def test_to_dict_includes_impact_score(self):
        card = _make_news_card(impact_score=8)
        d = card.to_dict()
        assert d["impact_score"] == 8

    def test_interaction_initialized(self):
        card = _make_news_card()
        assert card.interaction["votes_up"] == 0
        assert card.interaction["votes_down"] == 0
        assert card.interaction["views"] == 0
        assert card.interaction["shares"] == 0


# --- ResearchCard specifics ---


class TestResearchCardBehavior:
    """Tests for ResearchCard-specific behavior."""

    def test_card_type_is_research(self):
        card = ResearchCard(topic="Test", source=_make_source(), user_id="u1")
        assert card.get_card_type() == "research"

    def test_default_research_depth(self):
        card = ResearchCard(topic="Test", source=_make_source(), user_id="u1")
        assert card.research_depth == "quick"

    def test_default_key_findings_empty(self):
        card = ResearchCard(topic="Test", source=_make_source(), user_id="u1")
        assert card.key_findings == []

    def test_default_sources_count_zero(self):
        card = ResearchCard(topic="Test", source=_make_source(), user_id="u1")
        assert card.sources_count == 0

    def test_to_dict_includes_research_depth(self):
        card = ResearchCard(
            topic="Test",
            source=_make_source(),
            user_id="u1",
            research_depth="detailed",
        )
        d = card.to_dict()
        assert d["research_depth"] == "detailed"


# --- UpdateCard specifics ---


class TestUpdateCardBehavior:
    """Tests for UpdateCard-specific behavior."""

    def test_card_type_is_update(self):
        card = UpdateCard(topic="Test", source=_make_source(), user_id="u1")
        assert card.get_card_type() == "update"

    def test_default_update_type(self):
        card = UpdateCard(topic="Test", source=_make_source(), user_id="u1")
        assert card.update_type == "new_stories"

    def test_default_count_zero(self):
        card = UpdateCard(topic="Test", source=_make_source(), user_id="u1")
        assert card.count == 0

    def test_since_is_set(self):
        card = UpdateCard(topic="Test", source=_make_source(), user_id="u1")
        assert card.since is not None

    def test_to_dict_includes_update_type(self):
        card = UpdateCard(
            topic="Test",
            source=_make_source(),
            user_id="u1",
            update_type="breaking",
        )
        d = card.to_dict()
        assert d["update_type"] == "breaking"


# --- OverviewCard specifics ---


class TestOverviewCardBehavior:
    """Tests for OverviewCard-specific behavior."""

    def test_card_type_is_overview(self):
        card = OverviewCard(source=_make_source(), user_id="u1")
        assert card.get_card_type() == "overview"

    def test_topic_is_news_overview(self):
        card = OverviewCard(source=_make_source(), user_id="u1")
        assert card.topic == "News Overview"

    def test_default_stats(self):
        card = OverviewCard(source=_make_source(), user_id="u1")
        assert card.stats["total_new"] == 0
        assert card.stats["breaking"] == 0

    def test_default_summary_empty(self):
        card = OverviewCard(source=_make_source(), user_id="u1")
        assert card.summary == ""

    def test_default_top_stories_empty(self):
        card = OverviewCard(source=_make_source(), user_id="u1")
        assert card.top_stories == []

    def test_to_dict_includes_stats(self):
        card = OverviewCard(source=_make_source(), user_id="u1")
        d = card.to_dict()
        assert "stats" in d

    def test_to_dict_includes_trend_analysis(self):
        card = OverviewCard(source=_make_source(), user_id="u1")
        d = card.to_dict()
        assert "trend_analysis" in d


# --- BaseCard abstract enforcement ---


class TestBaseCardAbstract:
    """Tests that BaseCard enforces abstract methods."""

    def test_cannot_instantiate_directly(self):
        with pytest.raises(TypeError):
            BaseCard(topic="Test", source=_make_source(), user_id="u1")

    def test_id_auto_generated(self):
        card = _make_news_card()
        assert card.id is not None
        assert len(card.id) > 0

    def test_custom_card_id(self):
        card = _make_news_card(card_id="custom-id-123")
        assert card.id == "custom-id-123"

    def test_set_progress_callback(self):
        card = _make_news_card()
        cb = Mock()
        card.set_progress_callback(cb)
        assert card.progress_callback is cb

    def test_update_progress_calls_callback(self):
        card = _make_news_card()
        cb = Mock()
        card.set_progress_callback(cb)
        card._update_progress("Testing", 50)
        cb.assert_called_once_with("Testing", 50, {})

    def test_update_progress_no_callback(self):
        card = _make_news_card()
        # Should not raise when no callback set
        card._update_progress("Testing", 50)

    def test_update_progress_with_metadata(self):
        card = _make_news_card()
        cb = Mock()
        card.set_progress_callback(cb)
        card._update_progress("Testing", 50, {"key": "val"})
        cb.assert_called_once_with("Testing", 50, {"key": "val"})


# --- generate_card_id ---


class TestGenerateCardId:
    """Tests for the generate_card_id utility."""

    def test_returns_string(self):
        assert isinstance(generate_card_id(), str)

    def test_unique(self):
        ids = {generate_card_id() for _ in range(100)}
        assert len(ids) == 100

    def test_non_empty(self):
        assert len(generate_card_id()) > 0
