"""
Tests for news/rating_system/storage.py

Tests cover:
- SQLRatingStorage initialization
- create() - rating creation
- get() - rating retrieval
- update() - rating modification
- delete() - rating removal
- list() - rating listing with filters
- get_user_rating() - user-specific rating retrieval
- upsert_rating() - create or update rating
- get_ratings_summary() - aggregated ratings
- get_user_ratings() - all user ratings
- _get_rating_distribution() - rating distribution helper
"""

import pytest
from unittest.mock import MagicMock, patch


class TestSQLRatingStorageInitialization:
    """Tests for SQLRatingStorage initialization."""

    def test_initialization_with_valid_session(self):
        """Test initialization with a valid session."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

        mock_session = MagicMock()
        storage = SQLRatingStorage(mock_session)

        assert storage._session is mock_session

    def test_initialization_without_session_raises_error(self):
        """Test that initialization without session raises ValueError."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

        with pytest.raises(ValueError, match="Session is required"):
            SQLRatingStorage(None)

    def test_session_property(self):
        """Test session property returns correct session."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

        mock_session = MagicMock()
        storage = SQLRatingStorage(mock_session)

        assert storage.session is mock_session


class TestCreate:
    """Tests for create() method."""

    @patch("local_deep_research.news.rating_system.storage.UserRating")
    def test_create_rating_success(self, mock_model_class):
        """create() must map rating_value to UserRating.relevance_vote
        (the SUT docstring documents this remap) and pass through
        user_id, item_id, item_type unchanged. The returned id is the
        str() of the persisted row's id."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

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

        mock_rating = MagicMock()
        mock_rating.id = 123
        mock_model_class.return_value = mock_rating

        storage = SQLRatingStorage(mock_session)

        data = {
            "user_id": "user123",
            "item_id": "item456",
            "item_type": "card",
            "rating_value": "up",
        }

        result = storage.create(data)

        assert result == "123"
        mock_session.add.assert_called_once_with(mock_rating)
        mock_session.commit.assert_called_once()

        # Verify the SUT mapped fields correctly into the UserRating ctor.
        kwargs = mock_model_class.call_args.kwargs
        assert kwargs["user_id"] == "user123"
        assert kwargs["item_id"] == "item456"
        assert kwargs["item_type"] == "card"
        assert kwargs["relevance_vote"] == "up", (
            "rating_value must be remapped to relevance_vote per SUT contract"
        )
        # SUT always passes quality_rating=data.get(...) — verify the key
        # is present and explicitly None when not supplied.
        assert kwargs["quality_rating"] is None

    @patch("local_deep_research.news.rating_system.storage.UserRating")
    def test_create_rating_with_default_item_type(self, mock_model_class):
        """Test rating creation uses default item_type."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

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

        mock_rating = MagicMock()
        mock_rating.id = 1
        mock_model_class.return_value = mock_rating

        storage = SQLRatingStorage(mock_session)

        data = {
            "user_id": "user123",
            "item_id": "item456",
            # No item_type provided
        }

        storage.create(data)

        # Check that item_type was passed as "card" (default)
        mock_model_class.assert_called_once()
        call_kwargs = mock_model_class.call_args[1]
        assert call_kwargs.get("item_type") == "card"


class TestGet:
    """Tests for get() method."""

    def test_get_existing_rating(self):
        """Test getting an existing rating."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

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

        mock_rating = MagicMock()
        mock_rating.id = 123
        mock_rating.user_id = "user123"
        mock_rating.item_id = "item456"
        mock_rating.item_type = "card"
        mock_rating.relevance_vote = "up"
        mock_rating.quality_rating = 4
        mock_rating.created_at = None
        mock_rating.updated_at = None

        mock_query = MagicMock()
        mock_query.filter_by.return_value.first.return_value = mock_rating
        mock_session.query.return_value = mock_query

        storage = SQLRatingStorage(mock_session)
        result = storage.get("123")

        assert result is not None
        assert result["id"] == 123
        assert result["user_id"] == "user123"
        assert result["item_id"] == "item456"

    def test_get_nonexistent_rating(self):
        """Test getting a nonexistent rating returns None."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

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

        mock_query = MagicMock()
        mock_query.filter_by.return_value.first.return_value = None
        mock_session.query.return_value = mock_query

        storage = SQLRatingStorage(mock_session)
        result = storage.get("999")

        assert result is None


class TestUpdate:
    """Tests for update() method."""

    def test_update_existing_rating(self):
        """Test updating an existing rating."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

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

        mock_rating = MagicMock()
        mock_query = MagicMock()
        mock_query.filter_by.return_value.first.return_value = mock_rating
        mock_session.query.return_value = mock_query

        storage = SQLRatingStorage(mock_session)
        result = storage.update("123", {"rating_value": "down"})

        assert result is True
        mock_session.commit.assert_called_once()

    def test_update_nonexistent_rating(self):
        """Test updating a nonexistent rating returns False."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

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

        mock_query = MagicMock()
        mock_query.filter_by.return_value.first.return_value = None
        mock_session.query.return_value = mock_query

        storage = SQLRatingStorage(mock_session)
        result = storage.update("999", {"rating_value": "down"})

        assert result is False


class TestDelete:
    """Tests for delete() method."""

    def test_delete_existing_rating(self):
        """Test deleting an existing rating."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

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

        mock_rating = MagicMock()
        mock_query = MagicMock()
        mock_query.filter_by.return_value.first.return_value = mock_rating
        mock_session.query.return_value = mock_query

        storage = SQLRatingStorage(mock_session)
        result = storage.delete("123")

        assert result is True
        mock_session.delete.assert_called_once_with(mock_rating)
        mock_session.commit.assert_called_once()

    def test_delete_nonexistent_rating(self):
        """Test deleting a nonexistent rating returns False."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

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

        mock_query = MagicMock()
        mock_query.filter_by.return_value.first.return_value = None
        mock_session.query.return_value = mock_query

        storage = SQLRatingStorage(mock_session)
        result = storage.delete("999")

        assert result is False


class TestList:
    """Tests for list() method."""

    def test_list_all_ratings(self):
        """Test listing all ratings."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

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

        mock_rating = MagicMock()
        mock_rating.id = 1
        mock_rating.user_id = "user123"
        mock_rating.item_id = "item456"
        mock_rating.item_type = "card"
        mock_rating.relevance_vote = "up"
        mock_rating.quality_rating = None
        mock_rating.created_at = None
        mock_rating.updated_at = None

        mock_query = MagicMock()
        mock_query.order_by.return_value.limit.return_value.offset.return_value.all.return_value = [
            mock_rating
        ]
        mock_session.query.return_value = mock_query

        storage = SQLRatingStorage(mock_session)
        result = storage.list()

        assert len(result) == 1
        assert result[0]["id"] == 1

    def test_list_with_user_filter(self):
        """Test listing ratings with user filter."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

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

        mock_query = MagicMock()
        mock_query.filter_by.return_value = mock_query
        mock_query.order_by.return_value.limit.return_value.offset.return_value.all.return_value = []
        mock_session.query.return_value = mock_query

        storage = SQLRatingStorage(mock_session)
        storage.list(filters={"user_id": "user123"})

        mock_query.filter_by.assert_called_with(user_id="user123")

    def test_list_with_item_filter(self):
        """Test listing ratings with item filter."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

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

        mock_query = MagicMock()
        mock_query.filter_by.return_value = mock_query
        mock_query.order_by.return_value.limit.return_value.offset.return_value.all.return_value = []
        mock_session.query.return_value = mock_query

        storage = SQLRatingStorage(mock_session)
        storage.list(filters={"item_id": "item456"})

        mock_query.filter_by.assert_called_with(item_id="item456")

    def test_list_with_pagination(self):
        """Test listing ratings with pagination."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

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

        mock_query = MagicMock()
        mock_query.order_by.return_value.limit.return_value.offset.return_value.all.return_value = []
        mock_session.query.return_value = mock_query

        storage = SQLRatingStorage(mock_session)
        storage.list(limit=50, offset=10)

        mock_query.order_by.return_value.limit.assert_called_once_with(50)
        mock_query.order_by.return_value.limit.return_value.offset.assert_called_once_with(
            10
        )


class TestGetUserRating:
    """Tests for get_user_rating() method."""

    @patch("local_deep_research.news.rating_system.storage.UserRating")
    def test_get_user_rating_found(self, mock_user_rating_class):
        """Test getting existing user rating."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

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

        mock_rating = MagicMock()
        mock_rating.to_dict.return_value = {
            "id": 1,
            "user_id": "user123",
            "item_id": "item456",
            "rating_value": "up",
        }

        # Set up mock for UserRating attributes
        mock_user_rating_class.card_id = MagicMock()
        mock_user_rating_class.news_item_id = MagicMock()

        mock_query = MagicMock()
        mock_query.filter_by.return_value.filter.return_value.first.return_value = mock_rating
        mock_session.query.return_value = mock_query

        storage = SQLRatingStorage(mock_session)
        result = storage.get_user_rating("user123", "item456", "relevance")

        assert result is not None
        assert result["user_id"] == "user123"

    @patch("local_deep_research.news.rating_system.storage.UserRating")
    def test_get_user_rating_not_found(self, mock_user_rating_class):
        """Test getting nonexistent user rating."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

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

        # Set up mock for UserRating attributes
        mock_user_rating_class.card_id = MagicMock()
        mock_user_rating_class.news_item_id = MagicMock()

        mock_query = MagicMock()
        mock_query.filter_by.return_value.filter.return_value.first.return_value = None
        mock_session.query.return_value = mock_query

        storage = SQLRatingStorage(mock_session)
        result = storage.get_user_rating("user123", "item456", "relevance")

        assert result is None


class TestUpsertRating:
    """Tests for upsert_rating() method."""

    def test_upsert_creates_new_rating(self):
        """Test upsert creates new rating when none exists."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

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

        storage = SQLRatingStorage(mock_session)
        storage.get_user_rating = MagicMock(return_value=None)
        storage.create = MagicMock(return_value="new-id-123")

        result = storage.upsert_rating(
            "user123", "item456", "relevance", "up", "card"
        )

        assert result == "new-id-123"
        storage.create.assert_called_once()

    def test_upsert_updates_existing_rating(self):
        """Test upsert updates existing rating."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

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

        storage = SQLRatingStorage(mock_session)
        storage.get_user_rating = MagicMock(return_value={"id": 123})
        storage.update = MagicMock(return_value=True)

        result = storage.upsert_rating(
            "user123", "item456", "relevance", "down", "card"
        )

        assert result == "123"
        storage.update.assert_called_once_with("123", {"rating_value": "down"})


class TestGetRatingsSummary:
    """Tests for get_ratings_summary() method."""

    def test_get_ratings_summary_for_card(self):
        """Test getting ratings summary for a card."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

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

        # Mock quality ratings
        mock_quality_rating = MagicMock()
        mock_quality_rating.rating_value = "4"

        # Mock relevance ratings
        mock_relevance_up = MagicMock()
        mock_relevance_up.rating_value = "up"
        mock_relevance_down = MagicMock()
        mock_relevance_down.rating_value = "down"

        mock_query = MagicMock()
        mock_query.filter_by.return_value = mock_query
        mock_query.all.side_effect = [
            [mock_quality_rating],  # Quality ratings
            [mock_relevance_up, mock_relevance_down],  # Relevance ratings
        ]
        mock_session.query.return_value = mock_query

        storage = SQLRatingStorage(mock_session)
        result = storage.get_ratings_summary("item456", "card")

        assert result["item_id"] == "item456"
        assert result["item_type"] == "card"
        assert "quality" in result
        assert "relevance" in result

    def test_get_ratings_summary_empty(self):
        """Test getting ratings summary when no ratings exist."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

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

        mock_query = MagicMock()
        mock_query.filter_by.return_value = mock_query
        mock_query.all.return_value = []
        mock_session.query.return_value = mock_query

        storage = SQLRatingStorage(mock_session)
        result = storage.get_ratings_summary("item456", "card")

        assert result["quality"]["count"] == 0
        assert result["quality"]["average"] == 0
        assert result["relevance"]["up_votes"] == 0
        assert result["relevance"]["down_votes"] == 0


class TestGetUserRatings:
    """Tests for get_user_ratings() method."""

    def test_get_user_ratings_all(self):
        """Test getting all ratings for a user."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

        mock_session = MagicMock()
        storage = SQLRatingStorage(mock_session)
        storage.list = MagicMock(return_value=[{"id": 1}, {"id": 2}])

        result = storage.get_user_ratings("user123")

        assert len(result) == 2
        storage.list.assert_called_once_with({"user_id": "user123"}, 100)

    def test_get_user_ratings_with_type(self):
        """Test getting user ratings filtered by type."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

        mock_session = MagicMock()
        storage = SQLRatingStorage(mock_session)
        storage.list = MagicMock(return_value=[])

        storage.get_user_ratings("user123", rating_type="relevance", limit=50)

        storage.list.assert_called_once_with(
            {"user_id": "user123", "rating_type": "relevance"}, 50
        )


class TestGetRatingDistribution:
    """Tests for _get_rating_distribution() helper."""

    def test_distribution_empty_list(self):
        """Test distribution with empty list."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

        mock_session = MagicMock()
        storage = SQLRatingStorage(mock_session)

        result = storage._get_rating_distribution([])

        assert result == {1: 0, 2: 0, 3: 0, 4: 0, 5: 0}

    def test_distribution_all_same_value(self):
        """Test distribution with all same values."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

        mock_session = MagicMock()
        storage = SQLRatingStorage(mock_session)

        result = storage._get_rating_distribution([5, 5, 5])

        assert result == {1: 0, 2: 0, 3: 0, 4: 0, 5: 3}

    def test_distribution_varied_values(self):
        """Test distribution with varied values."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

        mock_session = MagicMock()
        storage = SQLRatingStorage(mock_session)

        result = storage._get_rating_distribution([1, 2, 3, 3, 4, 5, 5])

        assert result == {1: 1, 2: 1, 3: 2, 4: 1, 5: 2}

    def test_distribution_ignores_out_of_range(self):
        """Test distribution ignores values outside 1-5."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

        mock_session = MagicMock()
        storage = SQLRatingStorage(mock_session)

        result = storage._get_rating_distribution([0, 1, 3, 6, 10])

        assert result == {1: 1, 2: 0, 3: 1, 4: 0, 5: 0}


class TestInheritance:
    """Tests for RatingStorage inheritance."""

    def test_inherits_from_rating_storage(self):
        """Test that SQLRatingStorage inherits from RatingStorage."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )
        from local_deep_research.news.core.storage import RatingStorage

        mock_session = MagicMock()
        storage = SQLRatingStorage(mock_session)

        assert isinstance(storage, RatingStorage)

    def test_has_generate_id_method(self):
        """Test that storage has generate_id method from BaseStorage."""
        from local_deep_research.news.rating_system.storage import (
            SQLRatingStorage,
        )

        mock_session = MagicMock()
        storage = SQLRatingStorage(mock_session)

        assert hasattr(storage, "generate_id")
        assert callable(storage.generate_id)
