"""
Deep behavioral tests for card_storage.py pure logic.
Tests field mapping, source extraction, extra_data handling,
card_to_dict conversion, filtering patterns, and version management.
"""

from datetime import datetime, timedelta, timezone


# --- Source info extraction ---


class TestSourceInfoExtraction:
    """Tests for extracting source info from nested or flat data."""

    def _extract_source(self, data):
        source_info = data.get("source")
        if isinstance(source_info, dict) and source_info:
            return {
                "source_type": source_info.get("type"),
                "source_id": source_info.get("source_id"),
                "created_from": source_info.get("created_from"),
            }
        # Fallback to flat fields
        return {
            "source_type": data.get("source_type"),
            "source_id": data.get("source_id"),
            "created_from": data.get("created_from"),
        }

    def test_nested_source(self):
        data = {
            "source": {
                "type": "news_topic",
                "source_id": "src-123",
                "created_from": "Topic subscription",
            }
        }
        result = self._extract_source(data)
        assert result["source_type"] == "news_topic"
        assert result["source_id"] == "src-123"
        assert result["created_from"] == "Topic subscription"

    def test_flat_source(self):
        data = {
            "source_type": "user_search",
            "source_id": "search-456",
            "created_from": "User search",
        }
        result = self._extract_source(data)
        assert result["source_type"] == "user_search"
        assert result["source_id"] == "search-456"

    def test_empty_source(self):
        data = {}
        result = self._extract_source(data)
        assert result["source_type"] is None
        assert result["source_id"] is None

    def test_none_source_field(self):
        data = {"source": None}
        result = self._extract_source(data)
        # None is not a dict, so falls back to flat
        assert result["source_type"] is None


# --- Card type mapping ---


class TestCardTypeMapping:
    """Tests for card_type extraction and default."""

    def _get_card_type(self, data):
        return data.get("card_type", data.get("type", "news"))

    def test_explicit_card_type(self):
        data = {"card_type": "overview"}
        assert self._get_card_type(data) == "overview"

    def test_fallback_to_type(self):
        data = {"type": "topic"}
        assert self._get_card_type(data) == "topic"

    def test_default_news(self):
        data = {}
        assert self._get_card_type(data) == "news"

    def test_card_type_takes_precedence(self):
        data = {"card_type": "overview", "type": "topic"}
        assert self._get_card_type(data) == "overview"


# --- Extra data building ---


class TestExtraDataBuilding:
    """Tests for building extra_data field."""

    def _build_extra_data(self, data, created_from=None):
        extra_data = data.get("extra_data", {}) or {}
        extra_data.update(
            {
                "user_id": data.get("user_id"),
                "parent_card_id": data.get("parent_card_id"),
                "created_from": created_from,
                "metadata": data.get("metadata", {}),
                "interaction": data.get("interaction", {}),
            }
        )
        return extra_data

    def test_includes_user_id(self):
        data = {"user_id": "u1"}
        result = self._build_extra_data(data)
        assert result["user_id"] == "u1"

    def test_includes_parent_card_id(self):
        data = {"parent_card_id": "parent-123"}
        result = self._build_extra_data(data)
        assert result["parent_card_id"] == "parent-123"

    def test_includes_created_from(self):
        result = self._build_extra_data({}, created_from="Test source")
        assert result["created_from"] == "Test source"

    def test_includes_metadata(self):
        data = {"metadata": {"key": "value"}}
        result = self._build_extra_data(data)
        assert result["metadata"] == {"key": "value"}

    def test_includes_interaction(self):
        data = {"interaction": {"views": 5}}
        result = self._build_extra_data(data)
        assert result["interaction"] == {"views": 5}

    def test_merges_with_existing_extra_data(self):
        data = {"extra_data": {"existing": "field"}, "user_id": "u1"}
        result = self._build_extra_data(data)
        assert result["existing"] == "field"
        assert result["user_id"] == "u1"

    def test_handles_none_extra_data(self):
        data = {"extra_data": None, "user_id": "u1"}
        result = self._build_extra_data(data)
        assert result["user_id"] == "u1"

    def test_default_empty_metadata(self):
        result = self._build_extra_data({})
        assert result["metadata"] == {}

    def test_default_empty_interaction(self):
        result = self._build_extra_data({})
        assert result["interaction"] == {}


# --- Title/topic mapping ---


class TestTitleTopicMapping:
    """Tests for title extraction from topic or title field."""

    def _get_title(self, data):
        return data.get("topic", data.get("title", "Untitled"))

    def test_from_topic(self):
        data = {"topic": "AI News"}
        assert self._get_title(data) == "AI News"

    def test_from_title(self):
        data = {"title": "Breaking News"}
        assert self._get_title(data) == "Breaking News"

    def test_topic_takes_precedence(self):
        data = {"topic": "Topic", "title": "Title"}
        assert self._get_title(data) == "Topic"

    def test_default_untitled(self):
        data = {}
        assert self._get_title(data) == "Untitled"


# --- URL mapping ---


class TestUrlMapping:
    """Tests for URL extraction."""

    def _get_url(self, data):
        return data.get("url", data.get("source_url"))

    def test_from_url(self):
        data = {"url": "https://example.com/article"}
        assert self._get_url(data) == "https://example.com/article"

    def test_fallback_to_source_url(self):
        data = {"source_url": "https://source.com"}
        assert self._get_url(data) == "https://source.com"

    def test_url_takes_precedence(self):
        data = {"url": "https://main.com", "source_url": "https://source.com"}
        assert self._get_url(data) == "https://main.com"

    def test_none_when_missing(self):
        data = {}
        assert self._get_url(data) is None


# --- Update field mapping ---


class TestUpdateFieldMapping:
    """Tests for update field mapping patterns."""

    def _map_update_fields(self, data, card_extra_data=None):
        result = {}
        card_extra_data = card_extra_data or {}

        if "is_pinned" in data:
            result["is_saved"] = data["is_pinned"]
            if data["is_pinned"]:
                result["saved_at"] = datetime.now(timezone.utc)

        if "last_viewed" in data:
            result["is_read"] = True
            result["read_at"] = data["last_viewed"]

        if "is_archived" in data:
            card_extra_data["is_archived"] = data["is_archived"]

        if "interaction" in data:
            card_extra_data["interaction"] = data["interaction"]

        result["extra_data"] = card_extra_data
        return result

    def test_pin_sets_is_saved(self):
        result = self._map_update_fields({"is_pinned": True})
        assert result["is_saved"] is True

    def test_pin_sets_saved_at(self):
        result = self._map_update_fields({"is_pinned": True})
        assert "saved_at" in result

    def test_unpin_no_saved_at(self):
        result = self._map_update_fields({"is_pinned": False})
        assert "saved_at" not in result

    def test_last_viewed_sets_is_read(self):
        view_time = datetime.now(timezone.utc)
        result = self._map_update_fields({"last_viewed": view_time})
        assert result["is_read"] is True
        assert result["read_at"] == view_time

    def test_is_archived_in_extra_data(self):
        result = self._map_update_fields({"is_archived": True})
        assert result["extra_data"]["is_archived"] is True

    def test_interaction_in_extra_data(self):
        result = self._map_update_fields({"interaction": {"votes": 10}})
        assert result["extra_data"]["interaction"] == {"votes": 10}


# --- Card to dict mapping ---


class TestCardToDictMapping:
    """Tests for _card_to_dict conversion."""

    def _card_to_dict(self, card_data, extra_data=None):
        extra_data = extra_data or {}
        return {
            "id": card_data.get("id"),
            "topic": card_data.get("title"),
            "title": card_data.get("title"),
            "summary": card_data.get("summary"),
            "content": card_data.get("content"),
            "url": card_data.get("url"),
            "source_name": card_data.get("source_name"),
            "source_type": card_data.get("source_type"),
            "source_id": card_data.get("source_id"),
            "category": card_data.get("category"),
            "tags": card_data.get("tags"),
            "card_type": card_data.get("card_type"),
            "is_read": card_data.get("is_read", False),
            "is_saved": card_data.get("is_saved", False),
            "is_pinned": card_data.get("is_saved", False),
            "user_id": extra_data.get("user_id"),
            "parent_card_id": extra_data.get("parent_card_id"),
            "created_from": extra_data.get("created_from"),
            "is_archived": extra_data.get("is_archived", False),
            "metadata": extra_data.get("metadata", {}),
            "interaction": extra_data.get("interaction", {}),
            "source": {
                "type": card_data.get("source_type"),
                "source_id": card_data.get("source_id"),
                "created_from": extra_data.get("created_from", ""),
                "metadata": extra_data.get("metadata", {}),
            },
        }

    def test_title_mapped_to_topic(self):
        result = self._card_to_dict({"title": "My Title"})
        assert result["topic"] == "My Title"

    def test_is_saved_mapped_to_is_pinned(self):
        result = self._card_to_dict({"is_saved": True})
        assert result["is_pinned"] is True

    def test_extra_data_fields_extracted(self):
        result = self._card_to_dict(
            {}, {"user_id": "u1", "parent_card_id": "p1"}
        )
        assert result["user_id"] == "u1"
        assert result["parent_card_id"] == "p1"

    def test_is_archived_default_false(self):
        result = self._card_to_dict({}, {})
        assert result["is_archived"] is False

    def test_source_object_built(self):
        result = self._card_to_dict(
            {"source_type": "news", "source_id": "s1"},
            {"created_from": "test"},
        )
        assert result["source"]["type"] == "news"
        assert result["source"]["source_id"] == "s1"
        assert result["source"]["created_from"] == "test"


# --- List filter patterns ---


class TestListFilterPatterns:
    """Tests for list filtering logic."""

    def _should_filter_card_type(self, filters):
        return filters and "card_type" in filters

    def _is_card_type_list(self, card_type_val):
        return isinstance(card_type_val, list)

    def test_filter_card_type_present(self):
        assert self._should_filter_card_type({"card_type": "news"}) is True

    def test_filter_card_type_absent(self):
        assert self._should_filter_card_type({"category": "Tech"}) is False

    def test_card_type_as_list(self):
        assert self._is_card_type_list(["news", "topic"]) is True

    def test_card_type_as_string(self):
        assert self._is_card_type_list("news") is False


class TestCategoryFilter:
    """Tests for category filtering."""

    def _filter_by_category(self, cards, category):
        return [c for c in cards if c.get("category") == category]

    def test_filter_matching(self):
        cards = [{"category": "Tech"}, {"category": "Science"}]
        result = self._filter_by_category(cards, "Tech")
        assert len(result) == 1
        assert result[0]["category"] == "Tech"

    def test_filter_no_match(self):
        cards = [{"category": "Tech"}]
        result = self._filter_by_category(cards, "Sports")
        assert len(result) == 0


# --- Recent cards cutoff ---


class TestRecentCardsCutoff:
    """Tests for get_recent time window calculation."""

    def _calc_cutoff(self, hours):
        return datetime.now(timezone.utc) - timedelta(hours=hours)

    def test_24_hours(self):
        cutoff = self._calc_cutoff(24)
        now = datetime.now(timezone.utc)
        diff = now - cutoff
        assert 23 < diff.total_seconds() / 3600 < 25

    def test_48_hours(self):
        cutoff = self._calc_cutoff(48)
        now = datetime.now(timezone.utc)
        diff = now - cutoff
        assert 47 < diff.total_seconds() / 3600 < 49


class TestCardIsRecent:
    """Tests for checking if card is within time window."""

    def _is_recent(self, discovered_at, cutoff):
        return discovered_at >= cutoff

    def test_recent_card(self):
        cutoff = datetime.now(timezone.utc) - timedelta(hours=24)
        discovered = datetime.now(timezone.utc) - timedelta(hours=1)
        assert self._is_recent(discovered, cutoff) is True

    def test_old_card(self):
        cutoff = datetime.now(timezone.utc) - timedelta(hours=24)
        discovered = datetime.now(timezone.utc) - timedelta(hours=48)
        assert self._is_recent(discovered, cutoff) is False


# --- Get by user post-filter ---


class TestGetByUserPostFilter:
    """Tests for user-based card filtering logic."""

    def _filter_by_user(self, cards, user_id):
        return [
            c
            for c in cards
            if c.get("user_id") == user_id and not c.get("is_archived", False)
        ]

    def test_filter_by_user_id(self):
        cards = [
            {"user_id": "u1", "title": "A"},
            {"user_id": "u2", "title": "B"},
        ]
        result = self._filter_by_user(cards, "u1")
        assert len(result) == 1
        assert result[0]["title"] == "A"

    def test_excludes_archived(self):
        cards = [
            {"user_id": "u1", "is_archived": True},
            {"user_id": "u1", "is_archived": False},
        ]
        result = self._filter_by_user(cards, "u1")
        assert len(result) == 1

    def test_missing_user_id_excluded(self):
        cards = [{"title": "No user"}]
        result = self._filter_by_user(cards, "u1")
        assert len(result) == 0


class TestPaginationSlice:
    """Tests for pagination slice logic."""

    def _paginate(self, items, offset, limit):
        return items[offset : offset + limit]

    def test_first_page(self):
        items = list(range(10))
        result = self._paginate(items, 0, 5)
        assert result == [0, 1, 2, 3, 4]

    def test_second_page(self):
        items = list(range(10))
        result = self._paginate(items, 5, 5)
        assert result == [5, 6, 7, 8, 9]

    def test_partial_page(self):
        items = list(range(7))
        result = self._paginate(items, 5, 5)
        assert result == [5, 6]

    def test_offset_beyond_items(self):
        items = list(range(5))
        result = self._paginate(items, 10, 5)
        assert result == []


# --- Version management ---


class TestVersionManagement:
    """Tests for version-related logic."""

    def _get_version_number(self, versions):
        return len(versions) + 1

    def test_first_version(self):
        assert self._get_version_number([]) == 1

    def test_second_version(self):
        assert self._get_version_number([{"v": 1}]) == 2

    def test_multiple_versions(self):
        versions = [{"v": 1}, {"v": 2}, {"v": 3}]
        assert self._get_version_number(versions) == 4


class TestVersionRecord:
    """Tests for version record structure."""

    def _create_version_record(self, version_id, version_number, version_data):
        return {
            "id": version_id,
            "version_number": version_number,
            "search_query": version_data.get("search_query"),
            "headline": version_data.get("headline"),
            "summary": version_data.get("summary"),
            "findings": version_data.get("findings"),
            "sources": version_data.get("sources"),
            "impact_score": version_data.get("impact_score"),
            "topics": version_data.get("topics"),
            "entities": version_data.get("entities"),
            "created_at": datetime.now(timezone.utc).isoformat(),
        }

    def test_has_id(self):
        record = self._create_version_record("v1", 1, {})
        assert record["id"] == "v1"

    def test_has_version_number(self):
        record = self._create_version_record("v1", 3, {})
        assert record["version_number"] == 3

    def test_extracts_headline(self):
        record = self._create_version_record("v1", 1, {"headline": "News"})
        assert record["headline"] == "News"

    def test_has_created_at(self):
        record = self._create_version_record("v1", 1, {})
        assert "created_at" in record


class TestLatestVersionInfo:
    """Tests for get_latest_version fallback."""

    def _get_latest_or_default(self, card_data):
        extra_data = card_data.get("metadata", {})
        if "latest_version" in extra_data:
            return extra_data["latest_version"]
        card_id = card_data.get("id", "unknown")
        return {
            "version_id": f"{card_id}_v1",
            "version_number": 1,
            "headline": card_data.get("title"),
            "summary": card_data.get("summary"),
            "card_id": card_id,
        }

    def test_returns_stored_version(self):
        card_data = {"metadata": {"latest_version": {"version_number": 3}}}
        result = self._get_latest_or_default(card_data)
        assert result["version_number"] == 3

    def test_fallback_default_version(self):
        card_data = {"id": "card-123", "title": "Title", "summary": "Summary"}
        result = self._get_latest_or_default(card_data)
        assert result["version_id"] == "card-123_v1"
        assert result["version_number"] == 1
        assert result["headline"] == "Title"


# --- Archive and pin helpers ---


class TestArchivePinHelpers:
    """Tests for archive_card and pin_card patterns."""

    def test_archive_data(self):
        data = {"is_archived": True}
        assert data["is_archived"] is True

    def test_pin_data(self):
        data = {"is_pinned": True}
        assert data["is_pinned"] is True

    def test_unpin_data(self):
        data = {"is_pinned": False}
        assert data["is_pinned"] is False


# --- Datetime isoformat handling ---


class TestDatetimeIsoformat:
    """Tests for datetime isoformat conversion."""

    def _to_iso_or_none(self, dt):
        return dt.isoformat() if dt else None

    def test_datetime_to_iso(self):
        dt = datetime(2025, 6, 15, 10, 30, tzinfo=timezone.utc)
        result = self._to_iso_or_none(dt)
        assert "2025-06-15" in result

    def test_none_returns_none(self):
        assert self._to_iso_or_none(None) is None
