# tests/unit/services/memory/integration/test_context_source.py
"""Tests for MemoryContextSource service."""

from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock, patch
from uuid import uuid4

import pytest

from app.services.context.types.memory import MemorySubtype
from app.services.memory.integration.context_source import (
    MemoryContextSource,
    MemoryFetchConfig,
    MemoryFetchResult,
    get_memory_context_source,
)

pytestmark = pytest.mark.asyncio(loop_scope="function")


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


@pytest.fixture
def context_source(mock_session: MagicMock) -> MemoryContextSource:
    """Create MemoryContextSource instance."""
    return MemoryContextSource(session=mock_session)


class TestMemoryFetchConfig:
    """Tests for MemoryFetchConfig."""

    def test_default_values(self) -> None:
        """Default config values should be set correctly."""
        config = MemoryFetchConfig()

        assert config.working_limit == 10
        assert config.episodic_limit == 10
        assert config.semantic_limit == 15
        assert config.procedural_limit == 5
        assert config.episodic_days_back == 30
        assert config.min_relevance == 0.3
        assert config.include_working is True
        assert config.include_episodic is True
        assert config.include_semantic is True
        assert config.include_procedural is True

    def test_custom_values(self) -> None:
        """Custom config values should be respected."""
        config = MemoryFetchConfig(
            working_limit=5,
            include_working=False,
        )

        assert config.working_limit == 5
        assert config.include_working is False


class TestMemoryFetchResult:
    """Tests for MemoryFetchResult."""

    def test_stores_results(self) -> None:
        """Result should store contexts and metadata."""
        result = MemoryFetchResult(
            contexts=[],
            by_type={"working": 0, "episodic": 5, "semantic": 3, "procedural": 0},
            fetch_time_ms=15.5,
            query="test query",
        )

        assert result.contexts == []
        assert result.by_type["episodic"] == 5
        assert result.fetch_time_ms == 15.5
        assert result.query == "test query"


class TestMemoryContextSource:
    """Tests for MemoryContextSource service."""

    async def test_fetch_context_empty_when_no_sources(
        self,
        context_source: MemoryContextSource,
    ) -> None:
        """fetch_context should return empty when all sources fail."""
        config = MemoryFetchConfig(
            include_working=False,
            include_episodic=False,
            include_semantic=False,
            include_procedural=False,
        )

        result = await context_source.fetch_context(
            query="test",
            project_id=uuid4(),
            config=config,
        )

        assert len(result.contexts) == 0
        assert result.by_type == {
            "working": 0,
            "episodic": 0,
            "semantic": 0,
            "procedural": 0,
        }

    @patch("app.services.memory.integration.context_source.WorkingMemory")
    async def test_fetch_working_memory(
        self,
        mock_working_cls: MagicMock,
        context_source: MemoryContextSource,
    ) -> None:
        """Should fetch working memory when session_id provided."""
        # Setup mock - both keys should match the query "task"
        mock_working = AsyncMock()
        mock_working.list_keys = AsyncMock(return_value=["current_task", "task_state"])
        mock_working.get = AsyncMock(side_effect=lambda k: {"key": k, "value": "test"})
        mock_working_cls.for_session = AsyncMock(return_value=mock_working)

        config = MemoryFetchConfig(
            include_episodic=False,
            include_semantic=False,
            include_procedural=False,
        )

        result = await context_source.fetch_context(
            query="task",  # Both keys contain "task"
            project_id=uuid4(),
            session_id="sess-123",
            config=config,
        )

        assert result.by_type["working"] == 2
        assert all(c.memory_subtype == MemorySubtype.WORKING for c in result.contexts)

    @patch("app.services.memory.integration.context_source.EpisodicMemory")
    async def test_fetch_episodic_memory(
        self,
        mock_episodic_cls: MagicMock,
        context_source: MemoryContextSource,
    ) -> None:
        """Should fetch episodic memory."""
        # Setup mock episode
        mock_episode = MagicMock()
        mock_episode.id = uuid4()
        mock_episode.task_description = "Completed login feature"
        mock_episode.task_type = "feature"
        mock_episode.outcome = MagicMock(value="success")
        mock_episode.importance_score = 0.8
        mock_episode.occurred_at = datetime.now(UTC)
        mock_episode.lessons_learned = []

        mock_episodic = AsyncMock()
        mock_episodic.search_similar = AsyncMock(return_value=[mock_episode])
        mock_episodic.get_recent = AsyncMock(return_value=[])
        mock_episodic_cls.create = AsyncMock(return_value=mock_episodic)

        config = MemoryFetchConfig(
            include_working=False,
            include_semantic=False,
            include_procedural=False,
        )

        result = await context_source.fetch_context(
            query="login",
            project_id=uuid4(),
            config=config,
        )

        assert result.by_type["episodic"] == 1
        assert result.contexts[0].memory_subtype == MemorySubtype.EPISODIC
        assert "Completed login feature" in result.contexts[0].content

    @patch("app.services.memory.integration.context_source.SemanticMemory")
    async def test_fetch_semantic_memory(
        self,
        mock_semantic_cls: MagicMock,
        context_source: MemoryContextSource,
    ) -> None:
        """Should fetch semantic memory."""
        # Setup mock fact
        mock_fact = MagicMock()
        mock_fact.id = uuid4()
        mock_fact.subject = "User"
        mock_fact.predicate = "prefers"
        mock_fact.object = "dark mode"
        mock_fact.confidence = 0.9

        mock_semantic = AsyncMock()
        mock_semantic.search_facts = AsyncMock(return_value=[mock_fact])
        mock_semantic_cls.create = AsyncMock(return_value=mock_semantic)

        config = MemoryFetchConfig(
            include_working=False,
            include_episodic=False,
            include_procedural=False,
        )

        result = await context_source.fetch_context(
            query="preferences",
            project_id=uuid4(),
            config=config,
        )

        assert result.by_type["semantic"] == 1
        assert result.contexts[0].memory_subtype == MemorySubtype.SEMANTIC
        assert "User prefers dark mode" in result.contexts[0].content

    @patch("app.services.memory.integration.context_source.ProceduralMemory")
    async def test_fetch_procedural_memory(
        self,
        mock_procedural_cls: MagicMock,
        context_source: MemoryContextSource,
    ) -> None:
        """Should fetch procedural memory."""
        # Setup mock procedure
        mock_proc = MagicMock()
        mock_proc.id = uuid4()
        mock_proc.name = "Deploy"
        mock_proc.trigger_pattern = "When deploying"
        mock_proc.steps = [{"action": "build"}, {"action": "test"}]
        mock_proc.success_rate = 0.9
        mock_proc.success_count = 9
        mock_proc.failure_count = 1

        mock_procedural = AsyncMock()
        mock_procedural.find_matching = AsyncMock(return_value=[mock_proc])
        mock_procedural_cls.create = AsyncMock(return_value=mock_procedural)

        config = MemoryFetchConfig(
            include_working=False,
            include_episodic=False,
            include_semantic=False,
        )

        result = await context_source.fetch_context(
            query="deploy",
            project_id=uuid4(),
            config=config,
        )

        assert result.by_type["procedural"] == 1
        assert result.contexts[0].memory_subtype == MemorySubtype.PROCEDURAL
        assert "Deploy" in result.contexts[0].content

    async def test_results_sorted_by_relevance(
        self,
        context_source: MemoryContextSource,
    ) -> None:
        """Results should be sorted by relevance score."""
        with (
            patch.object(context_source, "_fetch_episodic") as mock_ep,
            patch.object(context_source, "_fetch_semantic") as mock_sem,
        ):
            # Create contexts with different relevance scores
            from app.services.context.types.memory import MemoryContext

            ctx_low = MemoryContext(
                content="low relevance",
                source="test",
                relevance_score=0.3,
            )
            ctx_high = MemoryContext(
                content="high relevance",
                source="test",
                relevance_score=0.9,
            )

            mock_ep.return_value = [ctx_low]
            mock_sem.return_value = [ctx_high]

            config = MemoryFetchConfig(
                include_working=False,
                include_procedural=False,
            )

            result = await context_source.fetch_context(
                query="test",
                project_id=uuid4(),
                config=config,
            )

            # Higher relevance should come first
            assert result.contexts[0].relevance_score == 0.9
            assert result.contexts[1].relevance_score == 0.3

    @patch("app.services.memory.integration.context_source.WorkingMemory")
    async def test_fetch_all_working(
        self,
        mock_working_cls: MagicMock,
        context_source: MemoryContextSource,
    ) -> None:
        """fetch_all_working should return all working memory items."""
        mock_working = AsyncMock()
        mock_working.list_keys = AsyncMock(return_value=["key1", "key2", "key3"])
        mock_working.get = AsyncMock(return_value="value")
        mock_working_cls.for_session = AsyncMock(return_value=mock_working)

        contexts = await context_source.fetch_all_working(
            session_id="sess-123",
            project_id=uuid4(),
        )

        assert len(contexts) == 3
        assert all(c.memory_subtype == MemorySubtype.WORKING for c in contexts)


class TestGetMemoryContextSource:
    """Tests for factory function."""

    async def test_creates_instance(self) -> None:
        """Factory should create MemoryContextSource instance."""
        mock_session = MagicMock()

        source = await get_memory_context_source(mock_session)

        assert isinstance(source, MemoryContextSource)
