"""
Deep behavioral tests for BaseCard and all card subclasses.
Tests real logic: initialization, helper methods, serialization, and edge cases.
"""

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

import pytest

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


# --- CardSource tests ---


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

    def test_requires_type(self):
        """Test type is required."""
        with pytest.raises(TypeError):
            CardSource()

    def test_type_stored(self):
        source = CardSource(type="news_item")
        assert source.type == "news_item"

    def test_source_id_defaults_none(self):
        source = CardSource(type="news_item")
        assert source.source_id is None

    def test_created_from_defaults_empty(self):
        source = CardSource(type="news_item")
        assert source.created_from == ""

    def test_metadata_defaults_empty_dict(self):
        source = CardSource(type="news_item")
        assert source.metadata == {}

    def test_metadata_independent_per_instance(self):
        """Test metadata dicts are independent between instances."""
        s1 = CardSource(type="a")
        s2 = CardSource(type="b")
        s1.metadata["key"] = "value"
        assert "key" not in s2.metadata

    def test_all_fields(self):
        source = CardSource(
            type="user_search",
            source_id="search-123",
            created_from="manual",
            metadata={"custom": True},
        )
        assert source.type == "user_search"
        assert source.source_id == "search-123"
        assert source.created_from == "manual"
        assert source.metadata == {"custom": True}


# --- CardVersion tests ---


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

    def test_auto_generates_version_id_if_empty(self):
        """Test __post_init__ generates id when version_id is empty string."""
        v = CardVersion(
            version_id="",
            created_at=datetime.now(timezone.utc),
            content={"data": 1},
            query_used="test",
        )
        assert v.version_id != ""
        uuid.UUID(v.version_id)  # Should be valid UUID

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

    def test_stores_content(self):
        content = {"findings": ["a", "b"]}
        v = CardVersion(
            version_id="v1",
            created_at=datetime.now(timezone.utc),
            content=content,
            query_used="q",
        )
        assert v.content == content

    def test_stores_query_used(self):
        v = CardVersion(
            version_id="v1",
            created_at=datetime.now(timezone.utc),
            content={},
            query_used="artificial intelligence",
        )
        assert v.query_used == "artificial intelligence"

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

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


# --- NewsCard tests ---


class TestNewsCardInit:
    """Tests for NewsCard initialization and __post_init__."""

    def _make_source(self):
        return CardSource(type="news_item", source_id="src-1")

    def test_id_is_valid_uuid(self):
        card = NewsCard(topic="AI", source=self._make_source(), user_id="u1")
        uuid.UUID(card.id)

    def test_custom_card_id_used(self):
        card = NewsCard(
            topic="AI",
            source=self._make_source(),
            user_id="u1",
            card_id="custom-id",
        )
        assert card.id == "custom-id"

    def test_created_at_is_utc(self):
        card = NewsCard(topic="AI", source=self._make_source(), user_id="u1")
        assert card.created_at.tzinfo is not None

    def test_updated_at_set(self):
        card = NewsCard(topic="AI", source=self._make_source(), user_id="u1")
        assert card.updated_at is not None

    def test_interaction_initialized(self):
        card = NewsCard(topic="AI", source=self._make_source(), user_id="u1")
        assert card.interaction == {
            "votes_up": 0,
            "votes_down": 0,
            "views": 0,
            "shares": 0,
        }

    def test_headline_defaults_to_topic(self):
        card = NewsCard(
            topic="Big News", source=self._make_source(), user_id="u1"
        )
        assert card.headline == "Big News"

    def test_headline_override(self):
        card = NewsCard(
            topic="Topic",
            source=self._make_source(),
            user_id="u1",
            headline="Custom Headline",
        )
        assert card.headline == "Custom Headline"

    def test_default_category(self):
        card = NewsCard(topic="AI", source=self._make_source(), user_id="u1")
        assert card.category == "General"

    def test_default_impact_score(self):
        card = NewsCard(topic="AI", source=self._make_source(), user_id="u1")
        assert card.impact_score == 5

    def test_default_entities(self):
        card = NewsCard(topic="AI", source=self._make_source(), user_id="u1")
        assert card.entities == {
            "people": [],
            "places": [],
            "organizations": [],
        }

    def test_default_is_developing(self):
        card = NewsCard(topic="AI", source=self._make_source(), user_id="u1")
        assert card.is_developing is False

    def test_versions_empty_by_default(self):
        card = NewsCard(topic="AI", source=self._make_source(), user_id="u1")
        assert card.versions == []

    def test_get_card_type(self):
        card = NewsCard(topic="AI", source=self._make_source(), user_id="u1")
        assert card.get_card_type() == "news"


class TestNewsCardToDict:
    """Tests for NewsCard.to_dict()."""

    def _make_card(self, **kwargs):
        source = CardSource(type="news_item", source_id="src-1")
        defaults = {
            "topic": "Test",
            "source": source,
            "user_id": "u1",
        }
        defaults.update(kwargs)
        return NewsCard(**defaults)

    def test_includes_id(self):
        card = self._make_card()
        d = card.to_dict()
        assert "id" in d

    def test_includes_topic(self):
        card = self._make_card(topic="Science")
        d = card.to_dict()
        assert d["topic"] == "Science"

    def test_includes_card_type(self):
        card = self._make_card()
        d = card.to_dict()
        assert d["card_type"] == "news"

    def test_includes_headline(self):
        card = self._make_card(headline="Breaking!")
        d = card.to_dict()
        assert d["headline"] == "Breaking!"

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

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

    def test_includes_is_developing(self):
        card = self._make_card(is_developing=True)
        d = card.to_dict()
        assert d["is_developing"] is True

    def test_includes_source_info(self):
        card = self._make_card()
        d = card.to_dict()
        assert d["source"]["type"] == "news_item"
        assert d["source"]["source_id"] == "src-1"

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

    def test_includes_user_id(self):
        card = self._make_card(user_id="user42")
        d = card.to_dict()
        assert d["user_id"] == "user42"

    def test_includes_interaction(self):
        card = self._make_card()
        d = card.to_dict()
        assert "interaction" in d
        assert d["interaction"]["votes_up"] == 0

    def test_created_at_is_isoformat(self):
        card = self._make_card()
        d = card.to_dict()
        # Should be parseable ISO format
        datetime.fromisoformat(d["created_at"])


# --- BaseCard helper methods ---


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

    def _make_card(self):
        source = CardSource(type="news_item")
        return NewsCard(topic="Test", source=source, user_id="u1")

    def test_uses_headline_field(self):
        card = self._make_card()
        result = card._extract_headline({"headline": "Big Story"})
        assert result == "Big Story"

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

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

    def test_returns_empty_if_nothing(self):
        card = self._make_card()
        result = card._extract_headline({})
        assert result == ""

    def test_headline_takes_priority_over_title(self):
        card = self._make_card()
        result = card._extract_headline({"headline": "H", "title": "T"})
        assert result == "H"


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

    def _make_card(self):
        source = CardSource(type="news_item")
        return NewsCard(topic="Test", source=source, user_id="u1")

    def test_uses_summary_field(self):
        card = self._make_card()
        result = card._extract_summary({"summary": "A summary"})
        assert result == "A summary"

    def test_falls_back_to_current_knowledge(self):
        card = self._make_card()
        result = card._extract_summary({"current_knowledge": "Knowledge"})
        assert result == "Knowledge"

    def test_falls_back_to_formatted_findings_truncated(self):
        card = self._make_card()
        long_findings = "y" * 1000
        result = card._extract_summary({"formatted_findings": long_findings})
        assert len(result) <= 500

    def test_returns_empty_if_nothing(self):
        card = self._make_card()
        result = card._extract_summary({})
        assert result == ""


class TestCalculateImpact:
    """Tests for BaseCard._calculate_impact() scoring logic."""

    def _make_card(self):
        source = CardSource(type="news_item")
        return NewsCard(topic="Test", source=source, user_id="u1")

    def test_base_score_is_5_with_no_findings(self):
        card = self._make_card()
        score = card._calculate_impact({})
        assert score == 5

    def test_score_increases_with_findings(self):
        card = self._make_card()
        findings = [f"finding-{i}" for i in range(10)]
        score = card._calculate_impact({"findings": findings})
        assert score > 5

    def test_score_increases_with_sources(self):
        card = self._make_card()
        sources = [f"source-{i}" for i in range(9)]
        score = card._calculate_impact({"sources": sources})
        assert score > 5

    def test_score_capped_at_10(self):
        card = self._make_card()
        result = {"findings": list(range(100)), "sources": list(range(100))}
        score = card._calculate_impact(result)
        assert score == 10

    def test_score_minimum_is_1(self):
        card = self._make_card()
        score = card._calculate_impact({"findings": [], "sources": []})
        assert score >= 1

    def test_score_formula_5_findings_adds_1(self):
        card = self._make_card()
        # 5 findings = 5 + 1 = 6
        score = card._calculate_impact({"findings": [1, 2, 3, 4, 5]})
        assert score == 6

    def test_score_formula_3_sources_adds_1(self):
        card = self._make_card()
        # 3 sources = 5 + 1 = 6
        score = card._calculate_impact({"sources": [1, 2, 3]})
        assert score == 6

    def test_combined_scoring(self):
        card = self._make_card()
        # 10 findings + 6 sources = 5 + 2 + 2 = 9
        score = card._calculate_impact(
            {"findings": list(range(10)), "sources": list(range(6))}
        )
        assert score == 9


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

    def _make_card(self):
        source = CardSource(type="news_item")
        return NewsCard(topic="Test", source=source, user_id="u1")

    def test_uses_topics_field(self):
        card = self._make_card()
        result = card._extract_topics({"topics": ["AI", "ML"]})
        assert result == ["AI", "ML"]

    def test_extracts_from_query_when_no_topics(self):
        card = self._make_card()
        result = card._extract_topics(
            {"query": "artificial intelligence breakthroughs"}
        )
        # Only words > 4 chars
        assert "artificial" in result
        assert "intelligence" in result
        assert "breakthroughs" in result

    def test_limits_extracted_topics_to_5(self):
        card = self._make_card()
        long_query = " ".join([f"longword{i}" for i in range(20)])
        result = card._extract_topics({"query": long_query})
        assert len(result) <= 5

    def test_filters_short_words_from_query(self):
        card = self._make_card()
        result = card._extract_topics({"query": "the big new ai event today"})
        assert "the" not in result
        assert "big" not in result
        assert "new" not in result
        assert "ai" not in result

    def test_returns_empty_when_no_data(self):
        card = self._make_card()
        result = card._extract_topics({})
        assert result == []


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

    def _make_card(self):
        source = CardSource(type="news_item")
        return NewsCard(topic="Test", source=source, user_id="u1")

    def test_uses_entities_field(self):
        card = self._make_card()
        entities = {"people": ["Alice"], "places": [], "organizations": []}
        result = card._extract_entities({"entities": entities})
        assert result == entities

    def test_default_empty_entities(self):
        card = self._make_card()
        result = card._extract_entities({})
        assert result == {"people": [], "places": [], "organizations": []}


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

    def _make_card(self):
        source = CardSource(type="news_item")
        return NewsCard(topic="Test", source=source, user_id="u1")

    def test_returns_none_when_no_versions(self):
        card = self._make_card()
        assert card.get_latest_version() is None

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

    def test_returns_latest_by_created_at(self):
        card = self._make_card()
        v1 = CardVersion(
            version_id="v1",
            created_at=datetime(2024, 1, 1, tzinfo=timezone.utc),
            content={},
            query_used="q",
        )
        v2 = CardVersion(
            version_id="v2",
            created_at=datetime(2024, 6, 1, tzinfo=timezone.utc),
            content={},
            query_used="q",
        )
        v3 = CardVersion(
            version_id="v3",
            created_at=datetime(2024, 3, 1, tzinfo=timezone.utc),
            content={},
            query_used="q",
        )
        card.versions.extend([v1, v2, v3])
        assert card.get_latest_version() is v2


class TestProgressCallback:
    """Tests for progress callback mechanism."""

    def _make_card(self):
        source = CardSource(type="news_item")
        return NewsCard(topic="Test", source=source, user_id="u1")

    def test_callback_not_called_when_not_set(self):
        card = self._make_card()
        # Should not raise
        card._update_progress("msg")

    def test_callback_called_when_set(self):
        card = self._make_card()
        cb = Mock()
        card.set_progress_callback(cb)
        card._update_progress("searching", 50, {"step": 1})
        cb.assert_called_once_with("searching", 50, {"step": 1})

    def test_callback_default_metadata(self):
        card = self._make_card()
        cb = Mock()
        card.set_progress_callback(cb)
        card._update_progress("msg", 10)
        cb.assert_called_once_with("msg", 10, {})


# --- ResearchCard tests ---


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

    def _make_source(self):
        return CardSource(type="research")

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

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

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

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

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

    def test_to_dict_includes_findings(self):
        card = ResearchCard(
            topic="AI",
            source=self._make_source(),
            user_id="u1",
            key_findings=["finding1", "finding2"],
        )
        d = card.to_dict()
        assert d["key_findings"] == ["finding1", "finding2"]

    def test_to_dict_includes_sources_count(self):
        card = ResearchCard(
            topic="AI",
            source=self._make_source(),
            user_id="u1",
            sources_count=15,
        )
        d = card.to_dict()
        assert d["sources_count"] == 15


# --- UpdateCard tests ---


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

    def _make_source(self):
        return CardSource(type="update")

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

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

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

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

    def test_to_dict_includes_count(self):
        card = UpdateCard(
            topic="Updates",
            source=self._make_source(),
            user_id="u1",
            count=42,
        )
        d = card.to_dict()
        assert d["count"] == 42

    def test_to_dict_since_is_isoformat(self):
        card = UpdateCard(
            topic="Updates", source=self._make_source(), user_id="u1"
        )
        d = card.to_dict()
        datetime.fromisoformat(d["since"])


# --- OverviewCard tests ---


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

    def _make_source(self):
        return CardSource(type="overview")

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

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

    def test_default_stats(self):
        card = OverviewCard(source=self._make_source(), user_id="u1")
        assert card.stats == {
            "total_new": 0,
            "breaking": 0,
            "relevant": 0,
            "categories": {},
        }

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

    def test_to_dict_includes_stats(self):
        card = OverviewCard(
            source=self._make_source(),
            user_id="u1",
            stats={
                "total_new": 5,
                "breaking": 1,
                "relevant": 3,
                "categories": {},
            },
        )
        d = card.to_dict()
        assert d["stats"]["total_new"] == 5

    def test_to_dict_includes_trend_analysis(self):
        card = OverviewCard(
            source=self._make_source(),
            user_id="u1",
            trend_analysis="AI is trending",
        )
        d = card.to_dict()
        assert d["trend_analysis"] == "AI is trending"
