# tests/unit/services/memory/episodic/test_memory.py
"""Unit tests for EpisodicMemory class."""

from unittest.mock import AsyncMock, MagicMock
from uuid import uuid4

import pytest

from app.services.memory.episodic.memory import EpisodicMemory
from app.services.memory.episodic.retrieval import RetrievalStrategy
from app.services.memory.types import EpisodeCreate, Outcome, RetrievalResult


class TestEpisodicMemoryInit:
    """Tests for EpisodicMemory initialization."""

    def test_init_creates_recorder_and_retriever(self) -> None:
        """Test that init creates recorder and retriever."""
        mock_session = AsyncMock()
        memory = EpisodicMemory(session=mock_session)

        assert memory._recorder is not None
        assert memory._retriever is not None
        assert memory._session is mock_session

    def test_init_with_embedding_generator(self) -> None:
        """Test init with embedding generator."""
        mock_session = AsyncMock()
        mock_embedding_gen = AsyncMock()
        memory = EpisodicMemory(
            session=mock_session, embedding_generator=mock_embedding_gen
        )

        assert memory._embedding_generator is mock_embedding_gen

    @pytest.mark.asyncio
    async def test_create_factory_method(self) -> None:
        """Test create factory method."""
        mock_session = AsyncMock()
        memory = await EpisodicMemory.create(session=mock_session)

        assert memory is not None
        assert memory._session is mock_session


class TestEpisodicMemoryRecording:
    """Tests for episode recording methods."""

    @pytest.fixture
    def mock_session(self) -> AsyncMock:
        """Create a mock database session."""
        session = AsyncMock()
        session.add = MagicMock()
        session.flush = AsyncMock()
        session.refresh = AsyncMock()
        return session

    @pytest.fixture
    def memory(self, mock_session: AsyncMock) -> EpisodicMemory:
        """Create an EpisodicMemory instance."""
        return EpisodicMemory(session=mock_session)

    @pytest.mark.asyncio
    async def test_record_episode(
        self,
        memory: EpisodicMemory,
    ) -> None:
        """Test recording an episode."""
        episode_data = EpisodeCreate(
            project_id=uuid4(),
            session_id="test-session",
            task_type="test_task",
            task_description="Test description",
            actions=[{"action": "test"}],
            context_summary="Test context",
            outcome=Outcome.SUCCESS,
            outcome_details="Success",
            duration_seconds=30.0,
            tokens_used=100,
        )

        result = await memory.record_episode(episode_data)

        assert result.project_id == episode_data.project_id
        assert result.task_type == "test_task"
        assert result.outcome == Outcome.SUCCESS

    @pytest.mark.asyncio
    async def test_record_success(
        self,
        memory: EpisodicMemory,
    ) -> None:
        """Test convenience method for recording success."""
        project_id = uuid4()
        result = await memory.record_success(
            project_id=project_id,
            session_id="test-session",
            task_type="deployment",
            task_description="Deploy to production",
            actions=[{"step": "deploy"}],
            context_summary="Deploying v1.0",
            outcome_details="Deployed successfully",
            duration_seconds=60.0,
            tokens_used=200,
        )

        assert result.outcome == Outcome.SUCCESS
        assert result.task_type == "deployment"

    @pytest.mark.asyncio
    async def test_record_failure(
        self,
        memory: EpisodicMemory,
    ) -> None:
        """Test convenience method for recording failure."""
        project_id = uuid4()
        result = await memory.record_failure(
            project_id=project_id,
            session_id="test-session",
            task_type="deployment",
            task_description="Deploy to production",
            actions=[{"step": "deploy"}],
            context_summary="Deploying v1.0",
            error_details="Connection timeout",
            duration_seconds=30.0,
            tokens_used=100,
        )

        assert result.outcome == Outcome.FAILURE
        assert result.outcome_details == "Connection timeout"


class TestEpisodicMemoryRetrieval:
    """Tests for episode retrieval methods."""

    @pytest.fixture
    def mock_session(self) -> AsyncMock:
        """Create a mock database session."""
        session = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalars.return_value.all.return_value = []
        session.execute.return_value = mock_result
        return session

    @pytest.fixture
    def memory(self, mock_session: AsyncMock) -> EpisodicMemory:
        """Create an EpisodicMemory instance."""
        return EpisodicMemory(session=mock_session)

    @pytest.mark.asyncio
    async def test_search_similar(
        self,
        memory: EpisodicMemory,
    ) -> None:
        """Test semantic search."""
        project_id = uuid4()
        results = await memory.search_similar(project_id, "authentication bug")

        assert isinstance(results, list)

    @pytest.mark.asyncio
    async def test_get_recent(
        self,
        memory: EpisodicMemory,
    ) -> None:
        """Test getting recent episodes."""
        project_id = uuid4()
        results = await memory.get_recent(project_id, limit=5)

        assert isinstance(results, list)

    @pytest.mark.asyncio
    async def test_get_by_outcome(
        self,
        memory: EpisodicMemory,
    ) -> None:
        """Test getting episodes by outcome."""
        project_id = uuid4()
        results = await memory.get_by_outcome(project_id, Outcome.FAILURE, limit=5)

        assert isinstance(results, list)

    @pytest.mark.asyncio
    async def test_get_by_task_type(
        self,
        memory: EpisodicMemory,
    ) -> None:
        """Test getting episodes by task type."""
        project_id = uuid4()
        results = await memory.get_by_task_type(project_id, "code_review", limit=5)

        assert isinstance(results, list)

    @pytest.mark.asyncio
    async def test_get_important(
        self,
        memory: EpisodicMemory,
    ) -> None:
        """Test getting important episodes."""
        project_id = uuid4()
        results = await memory.get_important(project_id, limit=5, min_importance=0.8)

        assert isinstance(results, list)

    @pytest.mark.asyncio
    async def test_retrieve_with_full_result(
        self,
        memory: EpisodicMemory,
    ) -> None:
        """Test retrieve with full result metadata."""
        project_id = uuid4()
        result = await memory.retrieve(project_id, RetrievalStrategy.RECENCY, limit=10)

        assert isinstance(result, RetrievalResult)
        assert result.retrieval_type == "recency"


class TestEpisodicMemorySummarization:
    """Tests for episode summarization."""

    @pytest.fixture
    def mock_session(self) -> AsyncMock:
        """Create a mock database session."""
        session = AsyncMock()
        return session

    @pytest.fixture
    def memory(self, mock_session: AsyncMock) -> EpisodicMemory:
        """Create an EpisodicMemory instance."""
        return EpisodicMemory(session=mock_session)

    @pytest.mark.asyncio
    async def test_summarize_empty_list(
        self,
        memory: EpisodicMemory,
    ) -> None:
        """Test summarizing empty episode list."""
        summary = await memory.summarize_episodes([])
        assert "No episodes to summarize" in summary

    @pytest.mark.asyncio
    async def test_summarize_not_found(
        self,
        memory: EpisodicMemory,
        mock_session: AsyncMock,
    ) -> None:
        """Test summarizing when episodes not found."""
        # Mock get_by_id to return None
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = None
        mock_session.execute.return_value = mock_result

        summary = await memory.summarize_episodes([uuid4(), uuid4()])
        assert "No episodes found" in summary


class TestEpisodicMemoryStats:
    """Tests for episode statistics."""

    @pytest.fixture
    def mock_session(self) -> AsyncMock:
        """Create a mock database session."""
        session = AsyncMock()
        return session

    @pytest.fixture
    def memory(self, mock_session: AsyncMock) -> EpisodicMemory:
        """Create an EpisodicMemory instance."""
        return EpisodicMemory(session=mock_session)

    @pytest.mark.asyncio
    async def test_get_stats(
        self,
        memory: EpisodicMemory,
        mock_session: AsyncMock,
    ) -> None:
        """Test getting episode statistics."""
        # Mock empty result
        mock_result = MagicMock()
        mock_result.scalars.return_value.all.return_value = []
        mock_session.execute.return_value = mock_result

        stats = await memory.get_stats(uuid4())

        assert "total_count" in stats
        assert "success_count" in stats
        assert "failure_count" in stats

    @pytest.mark.asyncio
    async def test_count(
        self,
        memory: EpisodicMemory,
        mock_session: AsyncMock,
    ) -> None:
        """Test counting episodes."""
        # Mock result with 3 episodes
        mock_result = MagicMock()
        mock_result.scalars.return_value.all.return_value = [1, 2, 3]
        mock_session.execute.return_value = mock_result

        count = await memory.count(uuid4())
        assert count == 3


class TestEpisodicMemoryModification:
    """Tests for episode modification methods."""

    @pytest.fixture
    def mock_session(self) -> AsyncMock:
        """Create a mock database session."""
        session = AsyncMock()
        return session

    @pytest.fixture
    def memory(self, mock_session: AsyncMock) -> EpisodicMemory:
        """Create an EpisodicMemory instance."""
        return EpisodicMemory(session=mock_session)

    @pytest.mark.asyncio
    async def test_get_by_id_not_found(
        self,
        memory: EpisodicMemory,
        mock_session: AsyncMock,
    ) -> None:
        """Test get_by_id returns None when not found."""
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = None
        mock_session.execute.return_value = mock_result

        result = await memory.get_by_id(uuid4())
        assert result is None

    @pytest.mark.asyncio
    async def test_update_importance_not_found(
        self,
        memory: EpisodicMemory,
        mock_session: AsyncMock,
    ) -> None:
        """Test update_importance returns None when not found."""
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = None
        mock_session.execute.return_value = mock_result

        result = await memory.update_importance(uuid4(), 0.9)
        assert result is None

    @pytest.mark.asyncio
    async def test_delete_not_found(
        self,
        memory: EpisodicMemory,
        mock_session: AsyncMock,
    ) -> None:
        """Test delete returns False when not found."""
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = None
        mock_session.execute.return_value = mock_result

        result = await memory.delete(uuid4())
        assert result is False
