"""
Tests for base card classes in news/core/base_card.py.

Tests cover:
- CardSource dataclass
- CardVersion dataclass
- BaseCard abstract class methods
- NewsCard concrete implementation
- ResearchCard concrete implementation
- UpdateCard concrete implementation
- OverviewCard concrete implementation
"""

import pytest
from unittest.mock import patch, MagicMock
from datetime import datetime

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


@pytest.fixture
def fixed_time():
    """Return a fixed datetime for testing."""
    return datetime(2024, 1, 15, 12, 0, 0)


@pytest.fixture
def mock_utc_now(fixed_time):
    """Mock utc_now to return consistent timestamps."""
    with patch("local_deep_research.news.core.base_card.utc_now") as mock:
        mock.return_value = fixed_time
        yield fixed_time


@pytest.fixture
def mock_generate_card_id():
    """Mock card ID generation for consistent IDs."""
    with patch(
        "local_deep_research.news.core.base_card.generate_card_id"
    ) as mock:
        mock.return_value = "test_card_id_123"
        yield mock


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

    def test_basic_creation(self):
        """Test basic CardSource creation."""
        source = CardSource(type="news_item")

        assert source.type == "news_item"
        assert source.source_id is None
        assert source.created_from == ""
        assert source.metadata == {}

    def test_full_creation(self):
        """Test CardSource with all fields."""
        source = CardSource(
            type="user_search",
            source_id="search_123",
            created_from="User search: AI",
            metadata={"search_strategy": "quick"},
        )

        assert source.type == "user_search"
        assert source.source_id == "search_123"
        assert source.created_from == "User search: AI"
        assert source.metadata == {"search_strategy": "quick"}

    def test_metadata_default_is_independent(self):
        """Test metadata default doesn't share between instances."""
        source1 = CardSource(type="test")
        source2 = CardSource(type="test")

        source1.metadata["key"] = "value"

        assert "key" not in source2.metadata


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

    def test_basic_creation(self, fixed_time):
        """Test basic CardVersion creation."""
        version = CardVersion(
            version_id="v1",
            created_at=fixed_time,
            content={"data": "test"},
            query_used="test query",
        )

        assert version.version_id == "v1"
        assert version.created_at == fixed_time
        assert version.content == {"data": "test"}
        assert version.query_used == "test query"
        assert version.search_strategy is None

    def test_with_search_strategy(self, fixed_time):
        """Test CardVersion with search strategy."""
        version = CardVersion(
            version_id="v1",
            created_at=fixed_time,
            content={},
            query_used="query",
            search_strategy="detailed",
        )

        assert version.search_strategy == "detailed"

    def test_auto_generates_id_when_empty(
        self, fixed_time, mock_generate_card_id
    ):
        """Test version_id is auto-generated when empty."""
        version = CardVersion(
            version_id="",
            created_at=fixed_time,
            content={},
            query_used="query",
        )

        assert version.version_id == "test_card_id_123"


class TestNewsCard:
    """Tests for NewsCard concrete implementation."""

    def test_basic_creation(self, mock_utc_now, mock_generate_card_id):
        """Test basic NewsCard creation."""
        source = CardSource(type="news_item")
        card = NewsCard(
            topic="AI Development",
            source=source,
            user_id="user_123",
        )

        assert card.topic == "AI Development"
        assert card.user_id == "user_123"
        assert card.id == "test_card_id_123"
        assert card.headline == "AI Development"  # Defaults to topic

    def test_with_custom_headline(self, mock_utc_now, mock_generate_card_id):
        """Test NewsCard with custom headline."""
        source = CardSource(type="news_item")
        card = NewsCard(
            topic="AI",
            source=source,
            user_id="user_123",
            headline="Breaking: AI Achieves New Milestone",
        )

        assert card.headline == "Breaking: AI Achieves New Milestone"

    def test_get_card_type(self, mock_utc_now, mock_generate_card_id):
        """Test get_card_type returns 'news'."""
        source = CardSource(type="news_item")
        card = NewsCard(topic="test", source=source, user_id="user_123")

        assert card.get_card_type() == "news"

    def test_to_dict_includes_news_fields(
        self, mock_utc_now, mock_generate_card_id
    ):
        """Test to_dict includes news-specific fields."""
        source = CardSource(type="news_item")
        card = NewsCard(
            topic="AI",
            source=source,
            user_id="user_123",
            headline="Test Headline",
            summary="Test summary",
            category="Technology",
            impact_score=8,
        )

        result = card.to_dict()

        assert result["headline"] == "Test Headline"
        assert result["summary"] == "Test summary"
        assert result["category"] == "Technology"
        assert result["impact_score"] == 8
        assert result["card_type"] == "news"

    def test_default_values(self, mock_utc_now, mock_generate_card_id):
        """Test NewsCard default values."""
        source = CardSource(type="news_item")
        card = NewsCard(topic="test", source=source, user_id="user_123")

        assert card.category == "General"
        assert card.impact_score == 5
        assert card.is_developing is False
        assert card.time_ago == "recent"
        assert card.entities == {
            "people": [],
            "places": [],
            "organizations": [],
        }

    def test_interaction_initialized(self, mock_utc_now, mock_generate_card_id):
        """Test interaction tracking is initialized."""
        source = CardSource(type="news_item")
        card = NewsCard(topic="test", source=source, user_id="user_123")

        assert card.interaction == {
            "votes_up": 0,
            "votes_down": 0,
            "views": 0,
            "shares": 0,
        }


class TestResearchCard:
    """Tests for ResearchCard concrete implementation."""

    def test_basic_creation(self, mock_utc_now, mock_generate_card_id):
        """Test basic ResearchCard creation."""
        source = CardSource(type="user_search")
        card = ResearchCard(
            topic="Machine Learning",
            source=source,
            user_id="user_123",
        )

        assert card.topic == "Machine Learning"
        assert card.research_depth == "quick"
        assert card.key_findings == []
        assert card.sources_count == 0

    def test_get_card_type(self, mock_utc_now, mock_generate_card_id):
        """Test get_card_type returns 'research'."""
        source = CardSource(type="user_search")
        card = ResearchCard(topic="test", source=source, user_id="user_123")

        assert card.get_card_type() == "research"

    def test_to_dict_includes_research_fields(
        self, mock_utc_now, mock_generate_card_id
    ):
        """Test to_dict includes research-specific fields."""
        source = CardSource(type="user_search")
        card = ResearchCard(
            topic="test",
            source=source,
            user_id="user_123",
            research_depth="detailed",
            key_findings=["finding1", "finding2"],
            sources_count=10,
        )

        result = card.to_dict()

        assert result["research_depth"] == "detailed"
        assert result["key_findings"] == ["finding1", "finding2"]
        assert result["sources_count"] == 10
        assert result["card_type"] == "research"


class TestUpdateCard:
    """Tests for UpdateCard concrete implementation."""

    def test_basic_creation(self, mock_utc_now, mock_generate_card_id):
        """Test basic UpdateCard creation."""
        source = CardSource(type="subscription")
        card = UpdateCard(
            topic="Updates",
            source=source,
            user_id="user_123",
        )

        assert card.topic == "Updates"
        assert card.update_type == "new_stories"
        assert card.count == 0
        assert card.preview_items == []

    def test_since_timestamp_set(self, mock_utc_now, mock_generate_card_id):
        """Test since timestamp is set on creation."""
        source = CardSource(type="subscription")
        card = UpdateCard(topic="test", source=source, user_id="user_123")

        assert card.since == mock_utc_now

    def test_get_card_type(self, mock_utc_now, mock_generate_card_id):
        """Test get_card_type returns 'update'."""
        source = CardSource(type="subscription")
        card = UpdateCard(topic="test", source=source, user_id="user_123")

        assert card.get_card_type() == "update"

    def test_to_dict_includes_update_fields(
        self, mock_utc_now, mock_generate_card_id
    ):
        """Test to_dict includes update-specific fields."""
        source = CardSource(type="subscription")
        card = UpdateCard(
            topic="test",
            source=source,
            user_id="user_123",
            update_type="breaking",
            count=5,
            preview_items=[{"title": "Item 1"}],
        )

        result = card.to_dict()

        assert result["update_type"] == "breaking"
        assert result["count"] == 5
        assert result["preview_items"] == [{"title": "Item 1"}]
        assert "since" in result
        assert result["card_type"] == "update"


class TestOverviewCard:
    """Tests for OverviewCard concrete implementation."""

    def test_basic_creation(self, mock_utc_now, mock_generate_card_id):
        """Test basic OverviewCard creation."""
        source = CardSource(type="system")
        card = OverviewCard(
            source=source,
            user_id="user_123",
        )

        # Topic is set automatically for overview cards
        assert card.topic == "News Overview"

    def test_get_card_type(self, mock_utc_now, mock_generate_card_id):
        """Test get_card_type returns 'overview'."""
        source = CardSource(type="system")
        card = OverviewCard(source=source, user_id="user_123")

        assert card.get_card_type() == "overview"

    def test_default_stats(self, mock_utc_now, mock_generate_card_id):
        """Test default stats structure."""
        source = CardSource(type="system")
        card = OverviewCard(source=source, user_id="user_123")

        assert card.stats == {
            "total_new": 0,
            "breaking": 0,
            "relevant": 0,
            "categories": {},
        }

    def test_to_dict_includes_overview_fields(
        self, mock_utc_now, mock_generate_card_id
    ):
        """Test to_dict includes overview-specific fields."""
        source = CardSource(type="system")
        card = OverviewCard(
            source=source,
            user_id="user_123",
            summary="Daily news summary",
            top_stories=[{"title": "Top Story"}],
            trend_analysis="Trending: AI",
        )

        result = card.to_dict()

        assert result["summary"] == "Daily news summary"
        assert result["top_stories"] == [{"title": "Top Story"}]
        assert result["trend_analysis"] == "Trending: AI"
        assert result["card_type"] == "overview"


class TestBaseCardMethods:
    """Tests for BaseCard concrete methods (using NewsCard as implementation)."""

    def test_set_progress_callback(self, mock_utc_now, mock_generate_card_id):
        """Test setting progress callback."""
        source = CardSource(type="news_item")
        card = NewsCard(topic="test", source=source, user_id="user_123")

        callback = MagicMock()
        card.set_progress_callback(callback)

        assert card.progress_callback is callback

    def test_update_progress_calls_callback(
        self, mock_utc_now, mock_generate_card_id
    ):
        """Test _update_progress calls the callback."""
        source = CardSource(type="news_item")
        card = NewsCard(topic="test", source=source, user_id="user_123")

        callback = MagicMock()
        card.set_progress_callback(callback)
        card._update_progress("Processing...", 50, {"step": "analysis"})

        callback.assert_called_once_with(
            "Processing...", 50, {"step": "analysis"}
        )

    def test_update_progress_without_callback(
        self, mock_utc_now, mock_generate_card_id
    ):
        """Test _update_progress does nothing without callback."""
        source = CardSource(type="news_item")
        card = NewsCard(topic="test", source=source, user_id="user_123")

        # Should not raise
        card._update_progress("Message", 50)

    def test_to_base_dict(self, mock_utc_now, mock_generate_card_id):
        """Test to_base_dict includes common fields."""
        source = CardSource(
            type="news_item",
            source_id="src_123",
            created_from="Test source",
        )
        card = NewsCard(
            topic="Test Topic",
            source=source,
            user_id="user_123",
            parent_card_id="parent_456",
        )

        result = card.to_base_dict()

        assert result["id"] == "test_card_id_123"
        assert result["topic"] == "Test Topic"
        assert result["user_id"] == "user_123"
        assert result["parent_card_id"] == "parent_456"
        assert result["versions_count"] == 0
        assert "source" in result
        assert result["source"]["type"] == "news_item"

    def test_get_latest_version_empty(
        self, mock_utc_now, mock_generate_card_id
    ):
        """Test get_latest_version returns None when no versions."""
        source = CardSource(type="news_item")
        card = NewsCard(topic="test", source=source, user_id="user_123")

        assert card.get_latest_version() is None

    def test_get_latest_version_returns_most_recent(
        self, mock_utc_now, mock_generate_card_id
    ):
        """Test get_latest_version returns most recent version."""
        source = CardSource(type="news_item")
        card = NewsCard(topic="test", source=source, user_id="user_123")

        # Add versions manually
        v1 = CardVersion(
            version_id="v1",
            created_at=datetime(2024, 1, 1),
            content={},
            query_used="q1",
        )
        v2 = CardVersion(
            version_id="v2",
            created_at=datetime(2024, 1, 15),
            content={},
            query_used="q2",
        )
        card.versions = [v1, v2]

        latest = card.get_latest_version()

        assert latest.version_id == "v2"


class TestBaseCardHelperMethods:
    """Tests for BaseCard helper methods."""

    def test_extract_headline_from_headline(
        self, mock_utc_now, mock_generate_card_id
    ):
        """Test _extract_headline extracts from headline field."""
        source = CardSource(type="news_item")
        card = NewsCard(topic="test", source=source, user_id="user_123")

        result = {"headline": "Test Headline"}
        assert card._extract_headline(result) == "Test Headline"

    def test_extract_headline_from_title(
        self, mock_utc_now, mock_generate_card_id
    ):
        """Test _extract_headline falls back to title."""
        source = CardSource(type="news_item")
        card = NewsCard(topic="test", source=source, user_id="user_123")

        result = {"title": "Test Title"}
        assert card._extract_headline(result) == "Test Title"

    def test_extract_headline_from_query(
        self, mock_utc_now, mock_generate_card_id
    ):
        """Test _extract_headline falls back to query (truncated)."""
        source = CardSource(type="news_item")
        card = NewsCard(topic="test", source=source, user_id="user_123")

        result = {"query": "A" * 150}
        headline = card._extract_headline(result)
        assert len(headline) == 100

    def test_extract_summary(self, mock_utc_now, mock_generate_card_id):
        """Test _extract_summary extracts from multiple fields."""
        source = CardSource(type="news_item")
        card = NewsCard(topic="test", source=source, user_id="user_123")

        # Try summary field
        assert card._extract_summary({"summary": "Test"}) == "Test"
        # Try current_knowledge field
        assert (
            card._extract_summary({"current_knowledge": "Knowledge"})
            == "Knowledge"
        )
        # Try formatted_findings (truncated)
        long_findings = "A" * 600
        assert (
            len(card._extract_summary({"formatted_findings": long_findings}))
            == 500
        )

    def test_calculate_impact(self, mock_utc_now, mock_generate_card_id):
        """Test _calculate_impact calculates score correctly."""
        source = CardSource(type="news_item")
        card = NewsCard(topic="test", source=source, user_id="user_123")

        # Empty result
        assert card._calculate_impact({}) == 5

        # With findings and sources
        result = {
            "findings": ["f1", "f2", "f3", "f4", "f5", "f6"],
            "sources": ["s1", "s2", "s3", "s4"],
        }
        score = card._calculate_impact(result)
        assert 1 <= score <= 10

    def test_extract_topics_from_topics_field(
        self, mock_utc_now, mock_generate_card_id
    ):
        """Test _extract_topics uses topics field."""
        source = CardSource(type="news_item")
        card = NewsCard(topic="test", source=source, user_id="user_123")

        result = {"topics": ["AI", "ML", "Tech"]}
        assert card._extract_topics(result) == ["AI", "ML", "Tech"]

    def test_extract_topics_from_query(
        self, mock_utc_now, mock_generate_card_id
    ):
        """Test _extract_topics extracts from query when no topics."""
        source = CardSource(type="news_item")
        card = NewsCard(topic="test", source=source, user_id="user_123")

        result = {"query": "artificial intelligence machine learning"}
        topics = card._extract_topics(result)
        # Should extract words with length > 4
        assert "artificial" in topics
        assert "intelligence" in topics

    def test_extract_entities(self, mock_utc_now, mock_generate_card_id):
        """Test _extract_entities returns entities or default structure."""
        source = CardSource(type="news_item")
        card = NewsCard(topic="test", source=source, user_id="user_123")

        # With entities
        result = {"entities": {"people": ["John"], "places": ["NYC"]}}
        assert card._extract_entities(result) == {
            "people": ["John"],
            "places": ["NYC"],
        }

        # Without entities
        entities = card._extract_entities({})
        assert entities == {"people": [], "places": [], "organizations": []}


class TestBaseCardWithCustomId:
    """Tests for BaseCard with custom ID."""

    def test_uses_provided_card_id(self, mock_utc_now):
        """Test card uses provided card_id instead of generating."""
        source = CardSource(type="news_item")
        card = NewsCard(
            topic="test",
            source=source,
            user_id="user_123",
            card_id="custom_id_456",
        )

        assert card.id == "custom_id_456"

    def test_generates_id_when_not_provided(
        self, mock_utc_now, mock_generate_card_id
    ):
        """Test card generates ID when not provided."""
        source = CardSource(type="news_item")
        card = NewsCard(
            topic="test",
            source=source,
            user_id="user_123",
        )

        assert card.id == "test_card_id_123"
