from unittest.mock import AsyncMock, Mock, patch

import pytest

from src.broadie.mixins import PersistenceMixin


class TestPersistenceMixin:
    """Unit tests for PersistenceMixin class."""

    @pytest.fixture
    def mock_settings(self):
        """Mock settings for testing."""
        with patch("src.broadie.persistence.mixins.settings") as mock_settings:
            mock_settings.DATABASE_URL = "sqlite:///test.db"
            mock_settings.EMBEDDING_MODEL = "openai:text-embedding-3-small"
            mock_settings.MEMORY_DECAY_MINUTES = 60
            yield mock_settings

    @pytest.fixture
    def persistence_mixin(self, mock_settings):
        """Create PersistenceMixin instance for testing."""
        with (
            patch("src.broadie.persistence.mixins.initialize_database"),
            patch("src.broadie.persistence.mixins.PersistenceRepository"),
        ):
            mixin = PersistenceMixin()
            # Mock the repository
            mixin._repository = AsyncMock()
            yield mixin

    def test_persistence_mixin_init(self, mock_settings):
        """Test PersistenceMixin initialization."""
        with patch("src.broadie.persistence.mixins.initialize_database"):
            persistence_mixin = PersistenceMixin()
            assert persistence_mixin.url == "sqlite:///test.db"
            assert persistence_mixin.embedding_model == "openai:text-embedding-3-small"
            assert persistence_mixin.decay_minutes == 60
            assert persistence_mixin.checkpointer is None
            assert persistence_mixin.store is None
            assert persistence_mixin._exit_stack is not None

    @pytest.mark.asyncio
    async def test_init_checkpointer_none_url(self, mock_settings):
        """Test init_checkpointer with no DATABASE_URL."""
        mock_settings.DATABASE_URL = None

        with (
            patch("src.broadie.persistence.mixins.initialize_database"),
            patch("src.broadie.persistence.mixins.InMemorySaver") as mock_saver,
        ):
            persistence_mixin = PersistenceMixin()
            mock_instance = Mock()
            mock_saver.return_value = mock_instance

            result = await persistence_mixin.init_checkpointer()

            assert result == mock_instance
            assert persistence_mixin.checkpointer == mock_instance
            mock_saver.assert_called_once()

    @pytest.mark.asyncio
    async def test_init_checkpointer_sqlite(self, mock_settings):
        """Test init_checkpointer with SQLite URL."""
        mock_settings.DATABASE_URL = "sqlite:///test.db"

        with (
            patch("src.broadie.persistence.mixins.initialize_database"),
            patch("src.broadie.persistence.mixins.AsyncSqliteSaver") as mock_saver,
        ):
            persistence_mixin = PersistenceMixin()
            mock_context = AsyncMock()
            mock_instance = Mock()
            mock_saver.from_conn_string.return_value = mock_context
            mock_context.__aenter__.return_value = mock_instance

            await persistence_mixin.init_checkpointer()

            mock_saver.from_conn_string.assert_called_once()
            assert persistence_mixin.checkpointer == mock_instance

    @pytest.mark.asyncio
    async def test_init_checkpointer_postgres(self, mock_settings):
        """Test init_checkpointer with PostgreSQL URL."""
        mock_settings.DATABASE_URL = "postgresql://localhost/test"

        with (
            patch("src.broadie.persistence.mixins.initialize_database"),
            patch("src.broadie.persistence.mixins.AsyncPostgresSaver") as mock_saver,
        ):
            persistence_mixin = PersistenceMixin()
            mock_context = AsyncMock()
            mock_instance = Mock()
            mock_saver.from_conn_string.return_value = mock_context
            mock_context.__aenter__.return_value = mock_instance

            await persistence_mixin.init_checkpointer()

            mock_saver.from_conn_string.assert_called_once_with("postgresql://localhost/test")
            assert persistence_mixin.checkpointer == mock_instance

    @pytest.mark.asyncio
    async def test_init_checkpointer_unsupported_url(self, mock_settings):
        """Test init_checkpointer with unsupported URL scheme."""
        mock_settings.DATABASE_URL = "unsupported://localhost/test"

        with patch("src.broadie.persistence.mixins.initialize_database"):
            persistence_mixin = PersistenceMixin()

            with pytest.raises(ValueError, match="Unsupported DATABASE_URL schema"):
                await persistence_mixin.init_checkpointer()

    @pytest.mark.skip(reason="Test requires langchain_openai package which may not be installed")
    @pytest.mark.asyncio
    async def test_init_store_postgres(self, mock_settings):
        """Test init_store with PostgreSQL URL."""
        mock_settings.DATABASE_URL = "postgresql://localhost/test"

        with (
            patch("src.broadie.persistence.mixins.initialize_database"),
            patch("src.broadie.persistence.mixins.AsyncPostgresStore") as mock_store,
        ):
            persistence_mixin = PersistenceMixin()
            mock_context = AsyncMock()
            mock_instance = Mock()
            mock_store.from_conn_string.return_value = mock_context
            mock_context.__aenter__.return_value = mock_instance

            await persistence_mixin.init_store()

            mock_store.from_conn_string.assert_called_once()
            assert persistence_mixin.store == mock_instance

    @pytest.mark.skip(reason="Test requires langchain_openai package which may not be installed")
    @pytest.mark.asyncio
    async def test_init_store_default_inmemory(self, mock_settings):
        """Test init_store defaults to InMemoryStore for non-Postgres."""
        mock_settings.DATABASE_URL = "sqlite:///test.db"

        with (
            patch("src.broadie.persistence.mixins.initialize_database"),
            patch("src.broadie.persistence.mixins.InMemoryStore") as mock_store,
        ):
            persistence_mixin = PersistenceMixin()
            mock_instance = Mock()
            mock_store.return_value = mock_instance

            await persistence_mixin.init_store()

            mock_store.assert_called_once()
            assert persistence_mixin.store == mock_instance

    @pytest.mark.asyncio
    async def test_aclose(self, mock_settings):
        """Test aclose method."""
        with (
            patch("src.broadie.persistence.mixins.initialize_database"),
            patch("src.broadie.persistence.mixins.PersistenceRepository"),
        ):
            persistence_mixin = PersistenceMixin()
            persistence_mixin._exit_stack = AsyncMock()
            await persistence_mixin.aclose()
            persistence_mixin._exit_stack.aclose.assert_called_once()

    @pytest.mark.asyncio
    async def test_remember_user_fact(self, mock_settings):
        """Test remember_user_fact method."""
        with (
            patch("src.broadie.persistence.mixins.initialize_database"),
            patch("src.broadie.persistence.mixins.PersistenceRepository"),
        ):
            persistence_mixin = PersistenceMixin()
            persistence_mixin._repository = AsyncMock()
            persistence_mixin._repository.remember_user_fact.return_value = "fact-id-123"

            result = await persistence_mixin.remember_user_fact("user123", {"test": "data"})

            assert result == "fact-id-123"
            persistence_mixin._repository.remember_user_fact.assert_called_once_with("user123", {"test": "data"})

    @pytest.mark.asyncio
    async def test_recall_user_facts(self, mock_settings):
        """Test recall_user_facts method."""
        with (
            patch("src.broadie.persistence.mixins.initialize_database"),
            patch("src.broadie.persistence.mixins.PersistenceRepository"),
        ):
            persistence_mixin = PersistenceMixin()
            mock_facts = [Mock(), Mock()]
            persistence_mixin._repository = AsyncMock()
            persistence_mixin._repository.recall_user_facts.return_value = mock_facts

            result = await persistence_mixin.recall_user_facts("user123", "query")

            assert result == mock_facts
            persistence_mixin._repository.recall_user_facts.assert_called_once_with("user123", "query", 3)

    @pytest.mark.asyncio
    async def test_recall_user_facts_custom_limit(self, mock_settings):
        """Test recall_user_facts with custom limit."""
        with (
            patch("src.broadie.persistence.mixins.initialize_database"),
            patch("src.broadie.persistence.mixins.PersistenceRepository"),
        ):
            persistence_mixin = PersistenceMixin()
            persistence_mixin._repository = AsyncMock()
            persistence_mixin._repository.recall_user_facts.return_value = []

            await persistence_mixin.recall_user_facts("user123", "query", limit=5)

            persistence_mixin._repository.recall_user_facts.assert_called_once_with("user123", "query", 5)

    @pytest.mark.asyncio
    async def test_clear_user_facts(self, mock_settings):
        """Test clear_user_facts method."""
        with (
            patch("src.broadie.persistence.mixins.initialize_database"),
            patch("src.broadie.persistence.mixins.PersistenceRepository"),
        ):
            persistence_mixin = PersistenceMixin()
            persistence_mixin._repository = AsyncMock()

            await persistence_mixin.clear_user_facts("user123")

            persistence_mixin._repository.clear_user_facts.assert_called_once_with("user123")

    @pytest.mark.asyncio
    async def test_save_thread_message(self, mock_settings):
        """Test save_thread_message method."""
        with (
            patch("src.broadie.persistence.mixins.initialize_database"),
            patch("src.broadie.persistence.mixins.PersistenceRepository"),
        ):
            persistence_mixin = PersistenceMixin()
            persistence_mixin._repository = AsyncMock()
            persistence_mixin._repository.save_thread_message.return_value = "msg-id-123"

            result = await persistence_mixin.save_thread_message("thread123", {"content": "test"})

            assert result == "msg-id-123"
            # Check that the method was called with the message containing _created_at
            call_args = persistence_mixin._repository.save_thread_message.call_args
            assert call_args[0][0] == "thread123"
            assert "content" in call_args[0][1]
            assert "_created_at" in call_args[0][1]

    @pytest.mark.asyncio
    async def test_get_thread_history(self, mock_settings):
        """Test get_thread_history method."""
        with (
            patch("src.broadie.persistence.mixins.initialize_database"),
            patch("src.broadie.persistence.mixins.PersistenceRepository"),
        ):
            persistence_mixin = PersistenceMixin()
            mock_history = [Mock(), Mock()]
            persistence_mixin._repository = AsyncMock()
            persistence_mixin._repository.get_thread_history.return_value = mock_history

            result = await persistence_mixin.get_thread_history("thread123")

            assert result == mock_history
            persistence_mixin._repository.get_thread_history.assert_called_once_with("thread123", 50)

    @pytest.mark.asyncio
    async def test_get_thread_history_custom_limit(self, mock_settings):
        """Test get_thread_history with custom limit."""
        with (
            patch("src.broadie.persistence.mixins.initialize_database"),
            patch("src.broadie.persistence.mixins.PersistenceRepository"),
        ):
            persistence_mixin = PersistenceMixin()
            persistence_mixin._repository = AsyncMock()
            persistence_mixin._repository.get_thread_history.return_value = []

            await persistence_mixin.get_thread_history("thread123", limit=20)

            persistence_mixin._repository.get_thread_history.assert_called_once_with("thread123", 20)

    @pytest.mark.asyncio
    async def test_clear_thread_history(self, mock_settings):
        """Test clear_thread_history method."""
        with (
            patch("src.broadie.persistence.mixins.initialize_database"),
            patch("src.broadie.persistence.mixins.PersistenceRepository"),
        ):
            persistence_mixin = PersistenceMixin()
            persistence_mixin._repository = AsyncMock()

            await persistence_mixin.clear_thread_history("thread123")

            persistence_mixin._repository.clear_thread_history.assert_called_once_with("thread123")

    @pytest.mark.skip(reason="is_expired method removed in new implementation")
    def test_is_expired_no_timestamp(self):
        """Test is_expired - skipped."""
        pass

    @pytest.mark.skip(reason="is_expired method removed in new implementation")
    def test_is_expired_not_expired(self):
        """Test is_expired - skipped."""
        pass

    @pytest.mark.skip(reason="is_expired method removed in new implementation")
    def test_is_expired_expired(self):
        """Test is_expired - skipped."""
        pass

    @pytest.mark.skip(reason="start_decay_cleaner method removed in new implementation")
    def test_start_decay_cleaner(self):
        """Test start_decay_cleaner - skipped."""
        pass

    @pytest.mark.skip(reason="SQLite path extraction now handled differently")
    def test_sqlite_path_extraction(self):
        """Test SQLite path extraction - skipped."""
        pass
