# tests/unit/services/memory/episodic/test_recorder.py
"""Unit tests for EpisodeRecorder."""

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

import pytest

from app.models.memory.enums import EpisodeOutcome
from app.services.memory.episodic.recorder import EpisodeRecorder, _outcome_to_db
from app.services.memory.types import EpisodeCreate, Outcome


class TestOutcomeConversion:
    """Tests for outcome conversion functions."""

    def test_outcome_to_db_success(self) -> None:
        """Test converting success outcome."""
        result = _outcome_to_db(Outcome.SUCCESS)
        assert result == EpisodeOutcome.SUCCESS

    def test_outcome_to_db_failure(self) -> None:
        """Test converting failure outcome."""
        result = _outcome_to_db(Outcome.FAILURE)
        assert result == EpisodeOutcome.FAILURE

    def test_outcome_to_db_partial(self) -> None:
        """Test converting partial outcome."""
        result = _outcome_to_db(Outcome.PARTIAL)
        assert result == EpisodeOutcome.PARTIAL


class TestEpisodeRecorderImportanceCalculation:
    """Tests for importance score calculation."""

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

    @pytest.fixture
    def recorder(self, mock_session: AsyncMock) -> EpisodeRecorder:
        """Create a recorder with mocked session."""
        return EpisodeRecorder(session=mock_session)

    def test_calculate_importance_success_default(
        self, recorder: EpisodeRecorder
    ) -> None:
        """Test importance for successful episode (default)."""
        episode = EpisodeCreate(
            project_id=uuid4(),
            session_id="test-session",
            task_type="test",
            task_description="Test task",
            actions=[],
            context_summary="Context",
            outcome=Outcome.SUCCESS,
            outcome_details="",
            duration_seconds=10.0,
            tokens_used=100,
        )
        score = recorder._calculate_importance(episode)
        assert 0.0 <= score <= 1.0
        assert score == 0.5  # Base score for success

    def test_calculate_importance_failure_higher(
        self, recorder: EpisodeRecorder
    ) -> None:
        """Test that failures get higher importance."""
        episode = EpisodeCreate(
            project_id=uuid4(),
            session_id="test-session",
            task_type="test",
            task_description="Test task",
            actions=[],
            context_summary="Context",
            outcome=Outcome.FAILURE,
            outcome_details="Error occurred",
            duration_seconds=10.0,
            tokens_used=100,
        )
        score = recorder._calculate_importance(episode)
        assert score >= 0.7  # Failure adds 0.2 to base 0.5

    def test_calculate_importance_with_lessons(self, recorder: EpisodeRecorder) -> None:
        """Test that lessons increase importance."""
        episode = EpisodeCreate(
            project_id=uuid4(),
            session_id="test-session",
            task_type="test",
            task_description="Test task",
            actions=[],
            context_summary="Context",
            outcome=Outcome.SUCCESS,
            outcome_details="",
            duration_seconds=10.0,
            tokens_used=100,
            lessons_learned=["Lesson 1", "Lesson 2"],
        )
        score = recorder._calculate_importance(episode)
        assert score > 0.5  # Lessons add to importance

    def test_calculate_importance_long_duration(
        self, recorder: EpisodeRecorder
    ) -> None:
        """Test that longer tasks get higher importance."""
        episode = EpisodeCreate(
            project_id=uuid4(),
            session_id="test-session",
            task_type="test",
            task_description="Test task",
            actions=[],
            context_summary="Context",
            outcome=Outcome.SUCCESS,
            outcome_details="",
            duration_seconds=400.0,  # > 300 seconds
            tokens_used=100,
        )
        score = recorder._calculate_importance(episode)
        assert score > 0.5  # Long duration adds to importance

    def test_calculate_importance_clamped_to_max(
        self, recorder: EpisodeRecorder
    ) -> None:
        """Test that importance is clamped to 1.0 max."""
        episode = EpisodeCreate(
            project_id=uuid4(),
            session_id="test-session",
            task_type="test",
            task_description="Test task",
            actions=[],
            context_summary="Context",
            outcome=Outcome.FAILURE,  # +0.2
            outcome_details="Error",
            duration_seconds=400.0,  # +0.1
            tokens_used=2000,  # +0.05
            lessons_learned=["L1", "L2", "L3", "L4", "L5"],  # +0.15
        )
        score = recorder._calculate_importance(episode)
        assert score <= 1.0


class TestEpisodeRecorderEmbeddingText:
    """Tests for embedding text generation."""

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

    @pytest.fixture
    def recorder(self, mock_session: AsyncMock) -> EpisodeRecorder:
        """Create a recorder with mocked session."""
        return EpisodeRecorder(session=mock_session)

    def test_create_embedding_text_basic(self, recorder: EpisodeRecorder) -> None:
        """Test basic embedding text creation."""
        episode = EpisodeCreate(
            project_id=uuid4(),
            session_id="test-session",
            task_type="code_review",
            task_description="Review PR #123",
            actions=[],
            context_summary="Reviewing authentication changes",
            outcome=Outcome.SUCCESS,
            outcome_details="",
            duration_seconds=60.0,
            tokens_used=500,
        )
        text = recorder._create_embedding_text(episode)
        assert "code_review" in text
        assert "Review PR #123" in text
        assert "authentication" in text
        assert "success" in text

    def test_create_embedding_text_with_details(
        self, recorder: EpisodeRecorder
    ) -> None:
        """Test embedding text includes outcome details."""
        episode = EpisodeCreate(
            project_id=uuid4(),
            session_id="test-session",
            task_type="deployment",
            task_description="Deploy to production",
            actions=[],
            context_summary="Deploying v1.0.0",
            outcome=Outcome.FAILURE,
            outcome_details="Connection timeout to server",
            duration_seconds=30.0,
            tokens_used=200,
        )
        text = recorder._create_embedding_text(episode)
        assert "Connection timeout" in text

    def test_create_embedding_text_with_lessons(
        self, recorder: EpisodeRecorder
    ) -> None:
        """Test embedding text includes lessons learned."""
        episode = EpisodeCreate(
            project_id=uuid4(),
            session_id="test-session",
            task_type="debugging",
            task_description="Fix memory leak",
            actions=[],
            context_summary="Debugging memory issues",
            outcome=Outcome.SUCCESS,
            outcome_details="",
            duration_seconds=120.0,
            tokens_used=800,
            lessons_learned=["Always close file handles", "Use context managers"],
        )
        text = recorder._create_embedding_text(episode)
        assert "Always close file handles" in text
        assert "context managers" in text


class TestEpisodeRecorderRecord:
    """Tests for episode recording."""

    @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 recorder(self, mock_session: AsyncMock) -> EpisodeRecorder:
        """Create a recorder with mocked session."""
        return EpisodeRecorder(session=mock_session)

    @pytest.mark.asyncio
    async def test_record_creates_episode(
        self,
        recorder: EpisodeRecorder,
        mock_session: AsyncMock,
    ) -> None:
        """Test that record creates 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 recorder.record(episode_data)

        # Verify session methods were called
        mock_session.add.assert_called_once()
        mock_session.flush.assert_called_once()
        mock_session.refresh.assert_called_once()

        # Verify result
        assert result.project_id == episode_data.project_id
        assert result.session_id == episode_data.session_id
        assert result.task_type == episode_data.task_type
        assert result.outcome == Outcome.SUCCESS

    @pytest.mark.asyncio
    async def test_record_with_embedding_generator(
        self,
        mock_session: AsyncMock,
    ) -> None:
        """Test recording with embedding generator."""
        mock_embedding_gen = AsyncMock()
        mock_embedding_gen.generate = AsyncMock(return_value=[0.1] * 1536)

        recorder = EpisodeRecorder(
            session=mock_session, embedding_generator=mock_embedding_gen
        )

        episode_data = EpisodeCreate(
            project_id=uuid4(),
            session_id="test-session",
            task_type="test_task",
            task_description="Test description",
            actions=[],
            context_summary="Test context",
            outcome=Outcome.SUCCESS,
            outcome_details="",
            duration_seconds=10.0,
            tokens_used=50,
        )

        await recorder.record(episode_data)

        # Verify embedding generator was called
        mock_embedding_gen.generate.assert_called_once()


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

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

    @pytest.fixture
    def recorder(self, mock_session: AsyncMock) -> EpisodeRecorder:
        """Create a recorder with mocked session."""
        return EpisodeRecorder(session=mock_session)

    @pytest.mark.asyncio
    async def test_get_stats_empty(
        self,
        recorder: EpisodeRecorder,
        mock_session: AsyncMock,
    ) -> None:
        """Test stats for project with no episodes."""
        # Mock empty result
        mock_result = MagicMock()
        mock_result.scalars.return_value.all.return_value = []
        mock_session.execute.return_value = mock_result

        stats = await recorder.get_stats(uuid4())

        assert stats["total_count"] == 0
        assert stats["success_count"] == 0
        assert stats["failure_count"] == 0
        assert stats["partial_count"] == 0
        assert stats["avg_importance"] == 0.0

    @pytest.mark.asyncio
    async def test_count_by_project(
        self,
        recorder: EpisodeRecorder,
        mock_session: AsyncMock,
    ) -> None:
        """Test counting episodes by project."""
        # Mock result with 5 episodes
        mock_result = MagicMock()
        mock_result.scalars.return_value.all.return_value = [1, 2, 3, 4, 5]
        mock_session.execute.return_value = mock_result

        count = await recorder.count_by_project(uuid4())

        assert count == 5
