"""
Tests for the CardFactory class.

Tests cover:
- Card type registration validation
- Unknown card type handling
- Storage singleton pattern
- Card creation and loading
- User card retrieval with filters and pagination
- Recent cards retrieval
- Card update and delete delegation
- Card reconstruction from storage data
- News card creation from analysis
- Module-level convenience functions
"""

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


class TestCardFactoryRegisterCardType:
    """Tests for CardFactory.register_card_type() method."""

    def test_register_card_type_invalid(self):
        """Error on non-BaseCard subclass registration."""
        from local_deep_research.news.core.card_factory import CardFactory

        # Try to register a non-BaseCard class
        class NotACard:
            pass

        with pytest.raises(ValueError) as exc_info:
            CardFactory.register_card_type("invalid", NotACard)

        assert "must be a subclass of BaseCard" in str(exc_info.value)

    def test_register_card_type_valid(self):
        """Test registering a valid BaseCard subclass."""
        from local_deep_research.news.core.card_factory import CardFactory
        from local_deep_research.news.core.base_card import BaseCard
        from dataclasses import dataclass

        @dataclass
        class CustomCard(BaseCard):
            custom_field: str = "default"

            def get_card_type(self) -> str:
                return "custom"

            def to_dict(self):
                return self.to_base_dict()

        # Register should not raise
        CardFactory.register_card_type("test_custom", CustomCard)

        assert "test_custom" in CardFactory._card_types
        assert CardFactory._card_types["test_custom"] == CustomCard

        # Cleanup
        del CardFactory._card_types["test_custom"]


class TestCardFactoryCardTypes:
    """Tests for registered card types."""

    def test_registered_card_types(self):
        """Verify default card types are registered."""
        from local_deep_research.news.core.card_factory import CardFactory

        # Default types should be registered
        assert "news" in CardFactory._card_types
        assert "research" in CardFactory._card_types
        assert "update" in CardFactory._card_types
        assert "overview" in CardFactory._card_types


class TestCardFactoryCreateCard:
    """Tests for CardFactory.create_card() method."""

    def test_create_card_unknown_type(self):
        """ValueError on unknown card type."""
        from local_deep_research.news.core.card_factory import CardFactory
        from local_deep_research.news.core.base_card import CardSource

        source = CardSource(
            type="test",
            source_id="test-123",
            created_from="test",
        )

        with pytest.raises(ValueError) as exc_info:
            CardFactory.create_card(
                card_type="nonexistent_type",
                topic="Test Topic",
                source=source,
                user_id="user-123",
            )

        assert "Unknown card type" in str(exc_info.value)
        assert "nonexistent_type" in str(exc_info.value)
        assert "Available types" in str(exc_info.value)

    def test_create_card_generates_unique_id(self):
        """Test create_card generates a unique ID."""
        from local_deep_research.news.core.card_factory import CardFactory
        from local_deep_research.news.core.base_card import CardSource

        source = CardSource(
            type="test",
            source_id="test-123",
            created_from="test",
        )

        with patch.object(CardFactory, "get_storage") as mock_get_storage:
            mock_storage = MagicMock()
            mock_get_storage.return_value = mock_storage

            card = CardFactory.create_card(
                card_type="news",
                topic="Test Topic",
                source=source,
                user_id="user-123",
            )

            # Should have a UUID-format ID
            assert len(card.id) == 36
            assert card.id.count("-") == 4

    def test_create_card_saves_to_storage(self):
        """Test create_card saves the card to storage."""
        from local_deep_research.news.core.card_factory import CardFactory
        from local_deep_research.news.core.base_card import CardSource

        source = CardSource(
            type="test",
            source_id="test-123",
            created_from="test",
        )

        with patch.object(CardFactory, "get_storage") as mock_get_storage:
            mock_storage = MagicMock()
            mock_get_storage.return_value = mock_storage

            CardFactory.create_card(
                card_type="news",
                topic="Test Topic",
                source=source,
                user_id="user-123",
            )

            mock_storage.create.assert_called_once()

    def test_create_card_returns_correct_type(self):
        """Test create_card returns the correct card type."""
        from local_deep_research.news.core.card_factory import CardFactory
        from local_deep_research.news.core.base_card import (
            CardSource,
            NewsCard,
            ResearchCard,
        )

        source = CardSource(
            type="test",
            source_id="test-123",
            created_from="test",
        )

        with patch.object(CardFactory, "get_storage") as mock_get_storage:
            mock_storage = MagicMock()
            mock_get_storage.return_value = mock_storage

            news_card = CardFactory.create_card(
                card_type="news",
                topic="Test Topic",
                source=source,
                user_id="user-123",
            )
            assert isinstance(news_card, NewsCard)

            research_card = CardFactory.create_card(
                card_type="research",
                topic="Research Topic",
                source=source,
                user_id="user-123",
            )
            assert isinstance(research_card, ResearchCard)


class TestCardFactoryGetStorage:
    """Tests for CardFactory.get_storage() method.

    Note: get_storage() now requires a session parameter or Flask context.
    Without either, it raises RuntimeError.
    """

    def test_get_storage_with_session_creates_instance(self):
        """Test get_storage creates instance with provided session."""
        from local_deep_research.news.core.card_factory import CardFactory

        with patch(
            "local_deep_research.news.core.card_factory.SQLCardStorage"
        ) as MockStorage:
            mock_instance = MagicMock()
            MockStorage.return_value = mock_instance
            mock_session = MagicMock()

            storage = CardFactory.get_storage(session=mock_session)

            assert storage is mock_instance
            MockStorage.assert_called_once_with(mock_session)

    def test_get_storage_without_session_raises_error(self):
        """Test get_storage raises RuntimeError without session or Flask context."""
        from local_deep_research.news.core.card_factory import CardFactory

        # Without Flask context and without session, should raise RuntimeError
        with pytest.raises(RuntimeError) as exc_info:
            CardFactory.get_storage()

        assert "No database session available" in str(exc_info.value)

    def test_get_storage_with_explicit_session_preferred(self):
        """Test that explicit session parameter is used even if Flask context exists."""
        from local_deep_research.news.core.card_factory import CardFactory

        with patch(
            "local_deep_research.news.core.card_factory.SQLCardStorage"
        ) as MockStorage:
            mock_instance = MagicMock()
            MockStorage.return_value = mock_instance
            explicit_session = MagicMock()

            # When session is provided explicitly, it should be used
            storage = CardFactory.get_storage(session=explicit_session)

            assert storage is mock_instance
            MockStorage.assert_called_once_with(explicit_session)


class TestCardFactoryLoadCard:
    """Tests for CardFactory.load_card() method."""

    def test_load_card_existing(self):
        """Test loading an existing card."""
        from local_deep_research.news.core.card_factory import CardFactory

        card_data = {
            "id": "card-123",
            "topic": "Test Topic",
            "user_id": "user-456",
            "card_type": "news",
            "created_at": datetime.now(timezone.utc),
            "updated_at": datetime.now(timezone.utc),
            "source": {
                "type": "test",
                "source_id": "src-123",
                "created_from": "test",
                "metadata": {},
            },
        }

        with patch.object(CardFactory, "get_storage") as mock_get_storage:
            mock_storage = MagicMock()
            mock_storage.get.return_value = card_data
            mock_get_storage.return_value = mock_storage

            card = CardFactory.load_card("card-123")

            assert card is not None
            assert card.id == "card-123"
            mock_storage.get.assert_called_once_with("card-123")

    def test_load_card_nonexistent(self):
        """Test loading a non-existent card returns None."""
        from local_deep_research.news.core.card_factory import CardFactory

        with patch.object(CardFactory, "get_storage") as mock_get_storage:
            mock_storage = MagicMock()
            mock_storage.get.return_value = None
            mock_get_storage.return_value = mock_storage

            card = CardFactory.load_card("nonexistent-id")

            assert card is None


class TestCardFactoryGetUserCards:
    """Tests for CardFactory.get_user_cards() method."""

    def test_get_user_cards_filters_by_user_id(self):
        """Test get_user_cards filters by user_id."""
        from local_deep_research.news.core.card_factory import CardFactory

        with patch.object(CardFactory, "get_storage") as mock_get_storage:
            mock_storage = MagicMock()
            mock_storage.list.return_value = []
            mock_get_storage.return_value = mock_storage

            CardFactory.get_user_cards("user-123")

            call_kwargs = mock_storage.list.call_args[1]
            assert call_kwargs["filters"]["user_id"] == "user-123"

    def test_get_user_cards_filters_by_card_types(self):
        """Test get_user_cards filters by card_types."""
        from local_deep_research.news.core.card_factory import CardFactory

        with patch.object(CardFactory, "get_storage") as mock_get_storage:
            mock_storage = MagicMock()
            mock_storage.list.return_value = []
            mock_get_storage.return_value = mock_storage

            CardFactory.get_user_cards(
                "user-123", card_types=["news", "research"]
            )

            call_kwargs = mock_storage.list.call_args[1]
            assert call_kwargs["filters"]["card_type"] == ["news", "research"]

    def test_get_user_cards_pagination(self):
        """Test get_user_cards with pagination."""
        from local_deep_research.news.core.card_factory import CardFactory

        with patch.object(CardFactory, "get_storage") as mock_get_storage:
            mock_storage = MagicMock()
            mock_storage.list.return_value = []
            mock_get_storage.return_value = mock_storage

            CardFactory.get_user_cards("user-123", limit=10, offset=20)

            call_kwargs = mock_storage.list.call_args[1]
            assert call_kwargs["limit"] == 10
            assert call_kwargs["offset"] == 20

    def test_get_user_cards_reconstructs_cards(self):
        """Test get_user_cards reconstructs cards from data."""
        from local_deep_research.news.core.card_factory import CardFactory

        card_data = {
            "id": "card-123",
            "topic": "Test Topic",
            "user_id": "user-123",
            "card_type": "news",
            "created_at": datetime.now(timezone.utc),
            "updated_at": datetime.now(timezone.utc),
            "source": {
                "type": "test",
                "source_id": "src-123",
                "created_from": "test",
                "metadata": {},
            },
        }

        with patch.object(CardFactory, "get_storage") as mock_get_storage:
            mock_storage = MagicMock()
            mock_storage.list.return_value = [card_data]
            mock_get_storage.return_value = mock_storage

            cards = CardFactory.get_user_cards("user-123")

            assert len(cards) == 1
            assert cards[0].id == "card-123"


class TestCardFactoryGetRecentCards:
    """Tests for CardFactory.get_recent_cards() method."""

    def test_get_recent_cards_filters_by_hours(self):
        """Test get_recent_cards filters by hours."""
        from local_deep_research.news.core.card_factory import CardFactory

        with patch.object(CardFactory, "get_storage") as mock_get_storage:
            mock_storage = MagicMock()
            mock_storage.get_recent.return_value = []
            mock_get_storage.return_value = mock_storage

            CardFactory.get_recent_cards(hours=48)

            call_kwargs = mock_storage.get_recent.call_args[1]
            assert call_kwargs["hours"] == 48

    def test_get_recent_cards_filters_by_card_types(self):
        """Test get_recent_cards filters by card_types."""
        from local_deep_research.news.core.card_factory import CardFactory

        with patch.object(CardFactory, "get_storage") as mock_get_storage:
            mock_storage = MagicMock()
            mock_storage.get_recent.return_value = []
            mock_get_storage.return_value = mock_storage

            CardFactory.get_recent_cards(card_types=["news"])

            call_kwargs = mock_storage.get_recent.call_args[1]
            assert call_kwargs["card_types"] == ["news"]

    def test_get_recent_cards_with_limit(self):
        """Test get_recent_cards with limit."""
        from local_deep_research.news.core.card_factory import CardFactory

        with patch.object(CardFactory, "get_storage") as mock_get_storage:
            mock_storage = MagicMock()
            mock_storage.get_recent.return_value = []
            mock_get_storage.return_value = mock_storage

            CardFactory.get_recent_cards(limit=25)

            call_kwargs = mock_storage.get_recent.call_args[1]
            assert call_kwargs["limit"] == 25


class TestCardFactoryUpdateCard:
    """Tests for CardFactory.update_card() method.

    Note: update_card now converts the card to dict and passes (id, data)
    to storage.update() instead of passing the card object directly.
    """

    def test_update_card_delegates_to_storage(self):
        """Test update_card delegates to storage."""
        from local_deep_research.news.core.card_factory import CardFactory

        mock_card = MagicMock()
        mock_card.id = "card-123"
        mock_card.to_dict.return_value = {"id": "card-123", "topic": "Test"}
        mock_card.interaction = {"views": 5}

        with patch.object(CardFactory, "get_storage") as mock_get_storage:
            mock_storage = MagicMock()
            mock_storage.update.return_value = True
            mock_get_storage.return_value = mock_storage

            result = CardFactory.update_card(mock_card)

            assert result is True
            # update is called with (id, data_dict) not (card)
            mock_storage.update.assert_called_once()
            call_args = mock_storage.update.call_args
            assert call_args[0][0] == "card-123"
            assert isinstance(call_args[0][1], dict)


class TestCardFactoryDeleteCard:
    """Tests for CardFactory.delete_card() method."""

    def test_delete_card_delegates_to_storage(self):
        """Test delete_card delegates to storage."""
        from local_deep_research.news.core.card_factory import CardFactory

        with patch.object(CardFactory, "get_storage") as mock_get_storage:
            mock_storage = MagicMock()
            mock_storage.delete.return_value = True
            mock_get_storage.return_value = mock_storage

            result = CardFactory.delete_card("card-123")

            assert result is True
            mock_storage.delete.assert_called_once_with("card-123")


class TestCardFactoryReconstructCard:
    """Tests for CardFactory._reconstruct_card() method."""

    def test_reconstruct_card_from_data(self):
        """Test _reconstruct_card rebuilds card from data."""
        from local_deep_research.news.core.card_factory import CardFactory

        card_data = {
            "id": "card-123",
            "topic": "Test Topic",
            "user_id": "user-456",
            "card_type": "news",
            "created_at": datetime.now(timezone.utc),
            "updated_at": datetime.now(timezone.utc),
            "source": {
                "type": "test",
                "source_id": "src-123",
                "created_from": "test",
                "metadata": {"key": "value"},
            },
            "versions": [],
            "metadata": {"extra": "data"},
            "interaction": {"views": 10},
        }

        card = CardFactory._reconstruct_card(card_data)

        assert card is not None
        assert card.id == "card-123"
        assert card.topic == "Test Topic"
        assert card.user_id == "user-456"
        assert card.source.type == "test"
        assert card.source.source_id == "src-123"

    def test_reconstruct_card_unknown_card_type(self):
        """Test _reconstruct_card returns None for unknown card type."""
        from local_deep_research.news.core.card_factory import CardFactory

        card_data = {
            "id": "card-123",
            "topic": "Test Topic",
            "user_id": "user-456",
            "card_type": "unknown_type",
            "created_at": datetime.now(timezone.utc),
            "updated_at": datetime.now(timezone.utc),
            "source": {},
        }

        card = CardFactory._reconstruct_card(card_data)

        assert card is None

    def test_reconstruct_card_handles_exceptions(self):
        """Test _reconstruct_card handles exceptions and returns None."""
        from local_deep_research.news.core.card_factory import CardFactory

        # Invalid data that would cause an exception - missing 'id' key
        card_data = {
            # Missing 'id' which is required
            "card_type": "news",
            "topic": "Test",
            "user_id": "user-123",
        }

        card = CardFactory._reconstruct_card(card_data)

        # Should return None due to KeyError on missing 'id'
        assert card is None

    def test_reconstruct_card_defaults_card_type_to_news(self):
        """Test _reconstruct_card defaults card_type to 'news'."""
        from local_deep_research.news.core.card_factory import CardFactory

        card_data = {
            "id": "card-123",
            "topic": "Test Topic",
            "user_id": "user-456",
            # No card_type specified
            "created_at": datetime.now(timezone.utc),
            "updated_at": datetime.now(timezone.utc),
            "source": {
                "type": "test",
                "source_id": "src-123",
                "created_from": "test",
                "metadata": {},
            },
        }

        card = CardFactory._reconstruct_card(card_data)

        assert card is not None
        assert card.get_card_type() == "news"

    def test_reconstruct_card_uses_source_defaults(self):
        """Test _reconstruct_card uses defaults for missing source fields."""
        from local_deep_research.news.core.card_factory import CardFactory

        card_data = {
            "id": "card-123",
            "topic": "Test Topic",
            "user_id": "user-456",
            "card_type": "news",
            "created_at": datetime.now(timezone.utc),
            "updated_at": datetime.now(timezone.utc),
            "source": {},  # Empty source
        }

        card = CardFactory._reconstruct_card(card_data)

        assert card is not None
        assert card.source.type == "unknown"
        assert card.source.created_from == ""
        assert card.source.metadata == {}


class TestCardFactoryCreateNewsCardFromAnalysis:
    """Tests for CardFactory.create_news_card_from_analysis() method."""

    def test_create_news_card_from_analysis(self):
        """Test create_news_card_from_analysis creates card from news item."""
        from local_deep_research.news.core.card_factory import CardFactory

        news_item = {
            "headline": "Breaking News",
            "category": "Tech",
            "summary": "This is a summary",
            "analysis": "Deep analysis",
            "impact_score": 8,
            "entities": {"people": ["John"]},
            "topics": ["technology"],
            "source_url": "https://example.com",
            "is_developing": True,
            "surprising_element": "Unexpected twist",
        }

        with patch.object(CardFactory, "create_card") as mock_create:
            mock_card = MagicMock()
            mock_create.return_value = mock_card

            result = CardFactory.create_news_card_from_analysis(
                news_item=news_item,
                source_search_id="search-123",
                user_id="user-456",
            )

            assert result is mock_card
            mock_create.assert_called_once()

            call_kwargs = mock_create.call_args[1]
            assert call_kwargs["card_type"] == "news"
            assert call_kwargs["topic"] == "Breaking News"
            assert call_kwargs["user_id"] == "user-456"
            assert call_kwargs["category"] == "Tech"
            assert call_kwargs["summary"] == "This is a summary"
            assert call_kwargs["impact_score"] == 8

    def test_create_news_card_from_analysis_merges_metadata(self):
        """Test create_news_card_from_analysis merges additional metadata."""
        from local_deep_research.news.core.card_factory import CardFactory

        news_item = {
            "headline": "News",
            "metadata": {"original": "data"},
        }

        additional_metadata = {"extra": "info"}

        with patch.object(CardFactory, "create_card") as mock_create:
            mock_card = MagicMock()
            mock_create.return_value = mock_card

            CardFactory.create_news_card_from_analysis(
                news_item=news_item,
                source_search_id="search-123",
                user_id="user-456",
                additional_metadata=additional_metadata,
            )

            call_kwargs = mock_create.call_args[1]
            # Metadata should include both original and additional
            assert call_kwargs["metadata"]["original"] == "data"
            assert call_kwargs["metadata"]["extra"] == "info"

    def test_create_news_card_from_analysis_uses_defaults(self):
        """Test create_news_card_from_analysis uses defaults for missing fields."""
        from local_deep_research.news.core.card_factory import CardFactory

        news_item = {}  # Empty news item

        with patch.object(CardFactory, "create_card") as mock_create:
            mock_card = MagicMock()
            mock_create.return_value = mock_card

            CardFactory.create_news_card_from_analysis(
                news_item=news_item,
                source_search_id="search-123",
                user_id="user-456",
            )

            call_kwargs = mock_create.call_args[1]
            assert call_kwargs["topic"] == "Untitled"
            assert call_kwargs["category"] == "Other"
            assert call_kwargs["summary"] == ""
            assert call_kwargs["impact_score"] == 5
            assert call_kwargs["is_developing"] is False

    def test_create_news_card_from_analysis_sets_source(self):
        """Test create_news_card_from_analysis sets correct source info."""
        from local_deep_research.news.core.card_factory import CardFactory

        news_item = {"headline": "Test"}

        with patch.object(CardFactory, "create_card") as mock_create:
            mock_card = MagicMock()
            mock_create.return_value = mock_card

            CardFactory.create_news_card_from_analysis(
                news_item=news_item,
                source_search_id="search-123",
                user_id="user-456",
            )

            call_kwargs = mock_create.call_args[1]
            source = call_kwargs["source"]
            assert source.type == "news_search"
            assert source.source_id == "search-123"
            assert source.created_from == "News analysis"


class TestModuleLevelConvenienceFunctions:
    """Tests for module-level convenience functions."""

    def test_create_card_function(self):
        """Test module-level create_card function."""
        from local_deep_research.news.core.card_factory import (
            create_card,
            CardFactory,
        )
        from local_deep_research.news.core.base_card import CardSource

        source = CardSource(
            type="test",
            source_id="test-123",
            created_from="test",
        )

        with patch.object(CardFactory, "create_card") as mock_create:
            mock_card = MagicMock()
            mock_create.return_value = mock_card

            result = create_card(
                card_type="news",
                topic="Test Topic",
                source=source,
                user_id="user-123",
            )

            assert result is mock_card
            # create_card takes card_type as positional, rest as kwargs
            mock_create.assert_called_once_with(
                "news",
                topic="Test Topic",
                source=source,
                user_id="user-123",
            )

    def test_load_card_function(self):
        """Test module-level load_card function."""
        from local_deep_research.news.core.card_factory import (
            load_card,
            CardFactory,
        )

        with patch.object(CardFactory, "load_card") as mock_load:
            mock_card = MagicMock()
            mock_load.return_value = mock_card

            result = load_card("card-123")

            assert result is mock_card
            mock_load.assert_called_once_with("card-123")


class TestModuleImports:
    """Tests for module imports."""

    def test_convenience_functions_importable(self):
        """Test convenience functions can be imported."""
        from local_deep_research.news.core.card_factory import (
            create_card,
            load_card,
        )

        assert create_card is not None
        assert load_card is not None
