"""
Tests for news/core/card_storage.py

Tests cover:
- SQLCardStorage initialization
- session property accessor
- CRUD operations (create, get, update, delete)
- list() with various filters
- get_by_user() functionality
- Version management (get_latest_version, add_version)
- update_latest_info()
- archive_card() and pin_card()
"""

import pytest
from unittest.mock import MagicMock, patch


class TestSQLCardStorageInit:
    """Tests for SQLCardStorage initialization."""

    def test_init_with_valid_session(self):
        """Test initialization with a valid session."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        storage = SQLCardStorage(mock_session)

        assert storage._session is mock_session

    def test_init_with_none_session_raises_error(self):
        """Test initialization with None session raises ValueError."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        with pytest.raises(ValueError) as exc_info:
            SQLCardStorage(None)

        assert "Session is required" in str(exc_info.value)

    def test_init_with_falsy_session_raises_error(self):
        """Test initialization with falsy session raises ValueError."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        # Empty dict is falsy but not None
        with pytest.raises(ValueError) as exc_info:
            SQLCardStorage({})

        assert "Session is required" in str(exc_info.value)


class TestSQLCardStorageSessionProperty:
    """Tests for SQLCardStorage session property."""

    def test_session_property_returns_session(self):
        """Test that session property returns the stored session."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        storage = SQLCardStorage(mock_session)

        assert storage.session is mock_session


class TestSQLCardStorageCreate:
    """Tests for SQLCardStorage.create() method."""

    def test_create_with_nested_source_info(self):
        """Test card creation with nested source info."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        storage = SQLCardStorage(mock_session)

        data = {
            "id": "card-123",
            "user_id": "user-456",
            "topic": "Test Topic",
            "card_type": "news",
            "source": {
                "type": "news_search",
                "source_id": "search-789",
                "created_from": "Analysis",
            },
        }

        with patch(
            "local_deep_research.news.core.card_storage.NewsCard"
        ) as MockNewsCard:
            mock_card = MagicMock()
            MockNewsCard.return_value = mock_card

            result = storage.create(data)

            assert result == "card-123"
            mock_context.add.assert_called_once_with(mock_card)
            mock_context.commit.assert_called_once()

    def test_create_with_flat_source_info(self):
        """Test card creation with flat source info."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        storage = SQLCardStorage(mock_session)

        data = {
            "user_id": "user-456",
            "topic": "Test Topic",
            "source_type": "manual",
            "source_id": "src-123",
            "created_from": "User input",
        }

        with patch(
            "local_deep_research.news.core.card_storage.NewsCard"
        ) as MockNewsCard:
            mock_card = MagicMock()
            MockNewsCard.return_value = mock_card

            result = storage.create(data)

            # Should generate an ID since none was provided
            assert isinstance(result, str)
            assert len(result) == 36  # UUID format
            mock_context.add.assert_called_once()
            mock_context.commit.assert_called_once()

    def test_create_uses_type_fallback_for_card_type(self):
        """Test that 'type' field is used as fallback for card_type."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        storage = SQLCardStorage(mock_session)

        data = {
            "id": "card-123",
            "user_id": "user-456",
            "topic": "Test Topic",
            "type": "research",  # Using 'type' instead of 'card_type'
        }

        with patch(
            "local_deep_research.news.core.card_storage.NewsCard"
        ) as MockNewsCard:
            mock_card = MagicMock()
            MockNewsCard.return_value = mock_card

            storage.create(data)

            # The NewsCard should be created with card_type="research"
            call_kwargs = MockNewsCard.call_args[1]
            assert call_kwargs["card_type"] == "research"

    def test_create_defaults_card_type_to_news(self):
        """Test that card_type defaults to 'news'."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        storage = SQLCardStorage(mock_session)

        data = {
            "id": "card-123",
            "user_id": "user-456",
            "topic": "Test Topic",
            # No card_type or type specified
        }

        with patch(
            "local_deep_research.news.core.card_storage.NewsCard"
        ) as MockNewsCard:
            mock_card = MagicMock()
            MockNewsCard.return_value = mock_card

            storage.create(data)

            call_kwargs = MockNewsCard.call_args[1]
            assert call_kwargs["card_type"] == "news"


class TestSQLCardStorageGet:
    """Tests for SQLCardStorage.get() method.

    Note: The get method now uses _card_to_dict for consistent mapping
    instead of calling card.to_dict() directly.
    """

    def test_get_existing_card(self):
        """Test retrieving an existing card."""
        from local_deep_research.news.core.card_storage import SQLCardStorage
        from datetime import datetime, timezone

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        # Create a properly structured mock card
        mock_card = MagicMock()
        mock_card.id = "card-123"
        mock_card.title = "Test Title"
        mock_card.summary = "Test Summary"
        mock_card.content = None
        mock_card.url = None
        mock_card.source_name = None
        mock_card.source_type = "manual"
        mock_card.source_id = None
        mock_card.category = None
        mock_card.tags = None
        mock_card.card_type = "news"
        mock_card.published_at = None
        mock_card.discovered_at = datetime(
            2024, 1, 15, 12, 0, 0, tzinfo=timezone.utc
        )
        mock_card.is_read = False
        mock_card.read_at = None
        mock_card.is_saved = False
        mock_card.saved_at = None
        mock_card.extra_data = {"user_id": "user-123"}

        mock_context.query.return_value.filter_by.return_value.first.return_value = mock_card

        storage = SQLCardStorage(mock_session)
        result = storage.get("card-123")

        # Verify key fields are correctly mapped
        assert result["id"] == "card-123"
        assert result["title"] == "Test Title"
        assert result["topic"] == "Test Title"  # title maps to topic
        assert result["user_id"] == "user-123"

    def test_get_nonexistent_card(self):
        """Test retrieving a non-existent card returns None."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        mock_context.query.return_value.filter_by.return_value.first.return_value = None

        storage = SQLCardStorage(mock_session)
        result = storage.get("nonexistent-id")

        assert result is None


class TestSQLCardStorageUpdate:
    """Tests for SQLCardStorage.update() method.

    Note: The update method maps card system fields to NewsCard model:
    - is_pinned → is_saved
    - is_archived → stored in extra_data
    - last_viewed → read_at
    """

    def test_update_existing_card_allowed_fields(self):
        """Test updating allowed fields on an existing card."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        mock_card = MagicMock()
        mock_card.extra_data = {}
        mock_context.query.return_value.filter_by.return_value.first.return_value = mock_card

        storage = SQLCardStorage(mock_session)
        result = storage.update(
            "card-123", {"is_archived": True, "is_pinned": True}
        )

        assert result is True
        # is_archived is stored in extra_data
        assert mock_card.extra_data["is_archived"] is True
        # is_pinned maps to is_saved
        assert mock_card.is_saved is True
        mock_context.commit.assert_called_once()

    def test_update_nonexistent_card_returns_false(self):
        """Test updating a non-existent card returns False."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        mock_context.query.return_value.filter_by.return_value.first.return_value = None

        storage = SQLCardStorage(mock_session)
        result = storage.update("nonexistent-id", {"is_archived": True})

        assert result is False
        mock_context.commit.assert_not_called()

    def test_update_ignores_non_updateable_fields(self):
        """Test that non-updateable fields are ignored."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        mock_card = MagicMock()
        mock_card.extra_data = {}
        original_title = mock_card.title
        mock_context.query.return_value.filter_by.return_value.first.return_value = mock_card

        storage = SQLCardStorage(mock_session)
        result = storage.update(
            "card-123",
            {
                "topic": "New Topic",  # Not directly updateable
                "user_id": "new-user",  # Not directly updateable
                "is_archived": True,  # Updateable (via extra_data)
            },
        )

        assert result is True
        # title should not be changed (topic maps to title)
        assert mock_card.title == original_title


class TestSQLCardStorageDelete:
    """Tests for SQLCardStorage.delete() method."""

    def test_delete_existing_card(self):
        """Test deleting an existing card."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        mock_card = MagicMock()
        mock_context.query.return_value.filter_by.return_value.first.return_value = mock_card

        storage = SQLCardStorage(mock_session)
        result = storage.delete("card-123")

        assert result is True
        mock_context.delete.assert_called_once_with(mock_card)
        mock_context.commit.assert_called_once()

    def test_delete_nonexistent_card(self):
        """Test deleting a non-existent card returns False."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        mock_context.query.return_value.filter_by.return_value.first.return_value = None

        storage = SQLCardStorage(mock_session)
        result = storage.delete("nonexistent-id")

        assert result is False
        mock_context.delete.assert_not_called()


class TestSQLCardStorageList:
    """Tests for SQLCardStorage.list() method."""

    def test_list_method_signature(self):
        """Test list method has expected signature."""
        from local_deep_research.news.core.card_storage import SQLCardStorage
        import inspect

        sig = inspect.signature(SQLCardStorage.list)
        params = list(sig.parameters.keys())

        assert "filters" in params
        assert "limit" in params
        assert "offset" in params

    def test_list_default_params(self):
        """Test list method has expected default parameters."""
        from local_deep_research.news.core.card_storage import SQLCardStorage
        import inspect

        sig = inspect.signature(SQLCardStorage.list)
        params = sig.parameters

        # Check default values
        assert params["filters"].default is None
        assert params["limit"].default == 100
        assert params["offset"].default == 0


class TestSQLCardStorageGetByUser:
    """Tests for SQLCardStorage.get_by_user() method."""

    def test_get_by_user_method_signature(self):
        """Test get_by_user method has expected signature."""
        from local_deep_research.news.core.card_storage import SQLCardStorage
        import inspect

        sig = inspect.signature(SQLCardStorage.get_by_user)
        params = list(sig.parameters.keys())

        assert "user_id" in params
        assert "limit" in params
        assert "offset" in params

    def test_get_by_user_default_params(self):
        """Test get_by_user has expected default parameters."""
        from local_deep_research.news.core.card_storage import SQLCardStorage
        import inspect

        sig = inspect.signature(SQLCardStorage.get_by_user)
        params = sig.parameters

        # Check default values
        assert params["limit"].default == 50
        assert params["offset"].default == 0


class TestSQLCardStorageGetLatestVersion:
    """Tests for SQLCardStorage.get_latest_version() method."""

    def test_get_latest_version_method_exists(self):
        """Test get_latest_version method exists and has expected signature."""
        from local_deep_research.news.core.card_storage import SQLCardStorage
        import inspect

        assert hasattr(SQLCardStorage, "get_latest_version")

        sig = inspect.signature(SQLCardStorage.get_latest_version)
        params = list(sig.parameters.keys())

        assert "card_id" in params


class TestSQLCardStorageAddVersion:
    """Tests for SQLCardStorage.add_version() method.

    Note: Version data is stored in the card's extra_data JSON field,
    not in a separate CardVersion table (CardVersion is a dataclass).
    """

    def test_add_version_creates_version(self):
        """Test add_version creates a new version in extra_data."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        mock_card = MagicMock()
        mock_card.extra_data = {}
        mock_context.query.return_value.filter_by.return_value.first.return_value = mock_card

        storage = SQLCardStorage(mock_session)

        version_data = {
            "id": "version-new",
            "search_query": "test query",
            "headline": "Test Headline",
            "summary": "Test summary",
            "impact_score": 7,
        }

        result = storage.add_version("card-123", version_data)

        assert result == "version-new"
        mock_context.commit.assert_called_once()

        # Verify card's title and summary are updated
        assert mock_card.title == "Test Headline"
        assert mock_card.summary == "Test summary"

        # Verify version is stored in extra_data
        assert "versions" in mock_card.extra_data
        assert len(mock_card.extra_data["versions"]) == 1
        assert mock_card.extra_data["versions"][0]["id"] == "version-new"

    def test_add_version_increments_version_number(self):
        """Test add_version increments version number correctly."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        mock_card = MagicMock()
        # Simulate 5 existing versions
        mock_card.extra_data = {
            "versions": [
                {"id": f"v{i}", "version_number": i} for i in range(1, 6)
            ]
        }
        mock_context.query.return_value.filter_by.return_value.first.return_value = mock_card

        storage = SQLCardStorage(mock_session)
        storage.add_version("card-123", {"headline": "Test"})

        # Version number should be 6 (5 existing + 1)
        new_version = mock_card.extra_data["versions"][-1]
        assert new_version["version_number"] == 6

    def test_add_version_raises_error_for_nonexistent_card(self):
        """Test add_version raises error for non-existent card."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        mock_context.query.return_value.filter_by.return_value.first.return_value = None

        storage = SQLCardStorage(mock_session)

        with pytest.raises(ValueError) as exc_info:
            storage.add_version("nonexistent", {"headline": "Test"})

        assert "Card nonexistent not found" in str(exc_info.value)

    def test_add_version_generates_id_if_not_provided(self):
        """Test add_version generates version ID if not provided."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        mock_card = MagicMock()
        mock_card.extra_data = {}
        mock_context.query.return_value.filter_by.return_value.first.return_value = mock_card

        storage = SQLCardStorage(mock_session)

        # No 'id' in version_data
        result = storage.add_version("card-123", {"headline": "Test"})

        # Should generate a UUID
        assert isinstance(result, str)
        assert len(result) == 36


class TestSQLCardStorageUpdateLatestInfo:
    """Tests for SQLCardStorage.update_latest_info() method.

    Note: Latest info is now stored in card's title/summary fields
    and in extra_data['latest_version'].
    """

    def test_update_latest_info_updates_card(self):
        """Test update_latest_info updates card fields and extra_data."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        mock_card = MagicMock()
        mock_card.extra_data = {}
        mock_context.query.return_value.filter_by.return_value.first.return_value = mock_card

        storage = SQLCardStorage(mock_session)
        version_data = {
            "id": "version-123",
            "headline": "Updated Headline",
            "summary": "Updated summary",
            "impact_score": 9,
        }

        result = storage.update_latest_info("card-123", version_data)

        assert result is True
        # Card display fields should be updated
        assert mock_card.title == "Updated Headline"
        assert mock_card.summary == "Updated summary"
        # Latest version info should be in extra_data
        assert mock_card.extra_data["latest_version"]["id"] == "version-123"
        assert mock_card.extra_data["latest_version"]["impact_score"] == 9
        mock_context.commit.assert_called_once()

    def test_update_latest_info_returns_false_for_nonexistent_card(self):
        """Test update_latest_info returns False for non-existent card."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        mock_context.query.return_value.filter_by.return_value.first.return_value = None

        storage = SQLCardStorage(mock_session)
        result = storage.update_latest_info("nonexistent", {"headline": "Test"})

        assert result is False
        mock_context.commit.assert_not_called()


class TestSQLCardStorageArchiveCard:
    """Tests for SQLCardStorage.archive_card() method.

    Note: is_archived is stored in extra_data JSON field since
    the NewsCard model doesn't have an is_archived column.
    """

    def test_archive_card_sets_is_archived_true(self):
        """Test archive_card sets is_archived to True in extra_data."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        mock_card = MagicMock()
        mock_card.extra_data = {}
        mock_context.query.return_value.filter_by.return_value.first.return_value = mock_card

        storage = SQLCardStorage(mock_session)
        result = storage.archive_card("card-123")

        assert result is True
        # is_archived is stored in extra_data
        assert mock_card.extra_data["is_archived"] is True

    def test_archive_card_returns_false_for_nonexistent_card(self):
        """Test archive_card returns False for non-existent card."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        mock_context.query.return_value.filter_by.return_value.first.return_value = None

        storage = SQLCardStorage(mock_session)
        result = storage.archive_card("nonexistent")

        assert result is False


class TestSQLCardStoragePinCard:
    """Tests for SQLCardStorage.pin_card() method.

    Note: is_pinned maps to is_saved in the NewsCard model.
    """

    def test_pin_card_sets_is_saved_true(self):
        """Test pin_card sets is_saved to True by default."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        mock_card = MagicMock()
        mock_card.extra_data = {}
        mock_context.query.return_value.filter_by.return_value.first.return_value = mock_card

        storage = SQLCardStorage(mock_session)
        result = storage.pin_card("card-123")

        assert result is True
        # is_pinned maps to is_saved
        assert mock_card.is_saved is True

    def test_pin_card_can_unpin(self):
        """Test pin_card can unpin a card."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        mock_card = MagicMock()
        mock_card.extra_data = {}
        mock_context.query.return_value.filter_by.return_value.first.return_value = mock_card

        storage = SQLCardStorage(mock_session)
        result = storage.pin_card("card-123", pinned=False)

        assert result is True
        assert mock_card.is_saved is False

    def test_pin_card_returns_false_for_nonexistent_card(self):
        """Test pin_card returns False for non-existent card."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        mock_context.query.return_value.filter_by.return_value.first.return_value = None

        storage = SQLCardStorage(mock_session)
        result = storage.pin_card("nonexistent")

        assert result is False


class TestSQLCardStorageInheritance:
    """Tests for SQLCardStorage inheritance from CardStorage."""

    def test_inherits_from_card_storage(self):
        """Test that SQLCardStorage inherits from CardStorage."""
        from local_deep_research.news.core.card_storage import SQLCardStorage
        from local_deep_research.news.core.storage import CardStorage

        assert issubclass(SQLCardStorage, CardStorage)

    def test_inherits_generate_id(self):
        """Test that SQLCardStorage inherits generate_id method."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        storage = SQLCardStorage(mock_session)

        result = storage.generate_id()

        assert isinstance(result, str)
        assert len(result) == 36


class TestSQLCardStorageGetRecent:
    """Tests for SQLCardStorage.get_recent() method."""

    def test_get_recent_method_exists(self):
        """Test get_recent method exists and has expected signature."""
        from local_deep_research.news.core.card_storage import SQLCardStorage
        import inspect

        assert hasattr(SQLCardStorage, "get_recent")

        sig = inspect.signature(SQLCardStorage.get_recent)
        params = list(sig.parameters.keys())

        assert "hours" in params
        assert "card_types" in params
        assert "limit" in params

    def test_get_recent_default_params(self):
        """Test get_recent has expected default parameters."""
        from local_deep_research.news.core.card_storage import SQLCardStorage
        import inspect

        sig = inspect.signature(SQLCardStorage.get_recent)
        params = sig.parameters

        assert params["hours"].default == 24
        assert params["card_types"].default is None
        assert params["limit"].default == 50

    def test_get_recent_returns_list(self):
        """Test get_recent returns a list of card dicts."""
        from local_deep_research.news.core.card_storage import SQLCardStorage
        from datetime import datetime, timezone

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        # Create mock cards
        mock_card1 = MagicMock()
        mock_card1.id = "card-1"
        mock_card1.title = "Test Card 1"
        mock_card1.summary = "Summary 1"
        mock_card1.content = None
        mock_card1.url = None
        mock_card1.source_name = None
        mock_card1.source_type = "news_search"
        mock_card1.source_id = "search-1"
        mock_card1.category = "Tech"
        mock_card1.tags = []
        mock_card1.card_type = "news"
        mock_card1.published_at = None
        mock_card1.discovered_at = datetime.now(timezone.utc)
        mock_card1.is_read = False
        mock_card1.read_at = None
        mock_card1.is_saved = False
        mock_card1.saved_at = None
        mock_card1.extra_data = {"user_id": "user-1"}

        mock_context.query.return_value.filter.return_value.order_by.return_value.limit.return_value.all.return_value = [
            mock_card1
        ]

        storage = SQLCardStorage(mock_session)
        result = storage.get_recent(hours=24, limit=10)

        assert isinstance(result, list)
        assert len(result) == 1
        assert result[0]["id"] == "card-1"
        assert result[0]["title"] == "Test Card 1"

    def test_get_recent_filters_by_card_types(self):
        """Test get_recent filters by card types when provided."""
        from local_deep_research.news.core.card_storage import SQLCardStorage

        mock_session = MagicMock()
        mock_context = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_context)
        mock_session.__exit__ = MagicMock(return_value=False)

        # Set up query chain
        mock_query = MagicMock()
        mock_context.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.limit.return_value = mock_query
        mock_query.all.return_value = []

        storage = SQLCardStorage(mock_session)
        storage.get_recent(hours=24, card_types=["news", "research"], limit=10)

        # Verify filter was called (checking the chain was used)
        assert mock_query.filter.called


class TestSQLCardStorageCardToDict:
    """Tests for SQLCardStorage._card_to_dict() helper method."""

    def test_card_to_dict_maps_fields_correctly(self):
        """Test _card_to_dict maps NewsCard fields to card system format."""
        from local_deep_research.news.core.card_storage import SQLCardStorage
        from datetime import datetime, timezone

        mock_session = MagicMock()
        storage = SQLCardStorage(mock_session)

        # Create a mock NewsCard
        mock_card = MagicMock()
        mock_card.id = "card-123"
        mock_card.title = "Test Title"
        mock_card.summary = "Test Summary"
        mock_card.content = "Test Content"
        mock_card.url = "https://example.com"
        mock_card.source_name = "Example News"
        mock_card.source_type = "rss"
        mock_card.source_id = "feed-1"
        mock_card.category = "Technology"
        mock_card.tags = ["tech", "ai"]
        mock_card.card_type = "news"
        mock_card.published_at = datetime(
            2024, 1, 15, 12, 0, 0, tzinfo=timezone.utc
        )
        mock_card.discovered_at = datetime(
            2024, 1, 15, 13, 0, 0, tzinfo=timezone.utc
        )
        mock_card.is_read = True
        mock_card.read_at = datetime(2024, 1, 15, 14, 0, 0, tzinfo=timezone.utc)
        mock_card.is_saved = True
        mock_card.saved_at = datetime(
            2024, 1, 15, 15, 0, 0, tzinfo=timezone.utc
        )
        mock_card.extra_data = {
            "user_id": "user-456",
            "parent_card_id": "parent-789",
            "created_from": "News analysis",
            "is_archived": False,
            "metadata": {"key": "value"},
            "interaction": {"views": 5},
        }

        result = storage._card_to_dict(mock_card)

        # Check field mappings
        assert result["id"] == "card-123"
        assert result["topic"] == "Test Title"  # title → topic
        assert result["title"] == "Test Title"
        assert result["is_pinned"] is True  # is_saved → is_pinned
        assert result["is_saved"] is True
        assert result["user_id"] == "user-456"  # from extra_data
        assert result["parent_card_id"] == "parent-789"
        assert result["is_archived"] is False
        assert result["source"]["type"] == "rss"
        assert result["interaction"]["views"] == 5
