# tests/unit/services/memory/semantic/test_memory.py
"""Unit tests for SemanticMemory class."""

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

import pytest

from app.services.memory.semantic.memory import SemanticMemory
from app.services.memory.types import FactCreate


def create_mock_fact_model(
    project_id=None,
    subject="FastAPI",
    predicate="uses",
    obj="Starlette",
    confidence=0.8,
):
    """Create a mock fact model for testing."""
    mock = MagicMock()
    mock.id = uuid4()
    mock.project_id = project_id
    mock.subject = subject
    mock.predicate = predicate
    mock.object = obj
    mock.confidence = confidence
    mock.source_episode_ids = []
    mock.first_learned = datetime.now(UTC)
    mock.last_reinforced = datetime.now(UTC)
    mock.reinforcement_count = 1
    mock.embedding = None
    mock.created_at = datetime.now(UTC)
    mock.updated_at = datetime.now(UTC)
    return mock


class TestSemanticMemoryInit:
    """Tests for SemanticMemory initialization."""

    def test_init_creates_memory(self) -> None:
        """Test that init creates memory instance."""
        mock_session = AsyncMock()
        memory = SemanticMemory(session=mock_session)

        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 = SemanticMemory(
            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 SemanticMemory.create(session=mock_session)

        assert memory is not None
        assert memory._session is mock_session


class TestSemanticMemoryStoreFact:
    """Tests for fact storage methods."""

    @pytest.fixture
    def mock_session(self) -> AsyncMock:
        """Create a mock database session."""
        session = AsyncMock()
        session.add = MagicMock()
        session.flush = AsyncMock()
        session.refresh = AsyncMock()
        # Mock no existing fact found
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = None
        session.execute.return_value = mock_result
        return session

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

    @pytest.mark.asyncio
    async def test_store_new_fact(
        self,
        memory: SemanticMemory,
        mock_session: AsyncMock,
    ) -> None:
        """Test storing a new fact."""
        fact_data = FactCreate(
            subject="Python",
            predicate="is_a",
            object="programming language",
            confidence=0.9,
            project_id=uuid4(),
        )

        result = await memory.store_fact(fact_data)

        assert result.subject == "Python"
        assert result.predicate == "is_a"
        assert result.object == "programming language"
        mock_session.add.assert_called_once()
        mock_session.flush.assert_called_once()

    @pytest.mark.asyncio
    async def test_store_fact_reinforces_existing(
        self,
        memory: SemanticMemory,
        mock_session: AsyncMock,
    ) -> None:
        """Test that storing duplicate fact reinforces existing."""
        # Mock existing fact found - needs to be found first
        existing_mock = create_mock_fact_model(confidence=0.7)
        find_result = MagicMock()
        find_result.scalar_one_or_none.return_value = existing_mock

        # Second find for reinforce_fact
        find_for_reinforce = MagicMock()
        find_for_reinforce.scalar_one_or_none.return_value = existing_mock

        # Mock update result - returns the updated mock
        updated_mock = create_mock_fact_model(confidence=0.8)
        update_result = MagicMock()
        update_result.scalar_one.return_value = updated_mock

        mock_session.execute.side_effect = [
            find_result,  # _find_existing_fact
            find_for_reinforce,  # reinforce_fact query
            update_result,  # reinforce_fact update
        ]

        fact_data = FactCreate(
            subject="FastAPI",
            predicate="uses",
            object="Starlette",
        )

        _ = await memory.store_fact(fact_data)

        # Should have called execute three times (find + find + update)
        assert mock_session.execute.call_count == 3


class TestSemanticMemorySearch:
    """Tests for fact search 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) -> SemanticMemory:
        """Create a SemanticMemory instance."""
        return SemanticMemory(session=mock_session)

    @pytest.mark.asyncio
    async def test_search_facts(
        self,
        memory: SemanticMemory,
    ) -> None:
        """Test searching for facts."""
        results = await memory.search_facts("Python programming")

        assert isinstance(results, list)

    @pytest.mark.asyncio
    async def test_search_facts_with_project_filter(
        self,
        memory: SemanticMemory,
    ) -> None:
        """Test searching for facts with project filter."""
        project_id = uuid4()
        results = await memory.search_facts("Python", project_id=project_id)

        assert isinstance(results, list)

    @pytest.mark.asyncio
    async def test_get_by_entity(
        self,
        memory: SemanticMemory,
    ) -> None:
        """Test getting facts by entity."""
        results = await memory.get_by_entity("FastAPI")

        assert isinstance(results, list)

    @pytest.mark.asyncio
    async def test_get_by_subject(
        self,
        memory: SemanticMemory,
    ) -> None:
        """Test getting facts by subject."""
        results = await memory.get_by_subject("Python")

        assert isinstance(results, list)

    @pytest.mark.asyncio
    async def test_get_by_id_not_found(
        self,
        memory: SemanticMemory,
        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


class TestSemanticMemoryReinforcement:
    """Tests for fact reinforcement."""

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

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

    @pytest.mark.asyncio
    async def test_reinforce_fact(
        self,
        memory: SemanticMemory,
        mock_session: AsyncMock,
    ) -> None:
        """Test reinforcing a fact."""
        existing_mock = create_mock_fact_model(confidence=0.7)

        # First query: find fact
        find_result = MagicMock()
        find_result.scalar_one_or_none.return_value = existing_mock

        # Second query: update fact
        updated_mock = create_mock_fact_model(confidence=0.8)
        update_result = MagicMock()
        update_result.scalar_one.return_value = updated_mock

        mock_session.execute.side_effect = [find_result, update_result]

        result = await memory.reinforce_fact(existing_mock.id, confidence_boost=0.1)

        assert result.confidence == 0.8

    @pytest.mark.asyncio
    async def test_reinforce_fact_not_found(
        self,
        memory: SemanticMemory,
        mock_session: AsyncMock,
    ) -> None:
        """Test reinforcing a non-existent fact raises error."""
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = None
        mock_session.execute.return_value = mock_result

        with pytest.raises(ValueError, match="Fact not found"):
            await memory.reinforce_fact(uuid4())

    @pytest.mark.asyncio
    async def test_deprecate_fact(
        self,
        memory: SemanticMemory,
        mock_session: AsyncMock,
    ) -> None:
        """Test deprecating a fact."""
        existing_mock = create_mock_fact_model(confidence=0.8)

        find_result = MagicMock()
        find_result.scalar_one_or_none.return_value = existing_mock

        deprecated_mock = create_mock_fact_model(confidence=0.0)
        update_result = MagicMock()
        update_result.scalar_one_or_none.return_value = deprecated_mock

        mock_session.execute.side_effect = [find_result, update_result]

        result = await memory.deprecate_fact(existing_mock.id, reason="Outdated")

        assert result is not None
        assert result.confidence == 0.0

    @pytest.mark.asyncio
    async def test_deprecate_fact_not_found(
        self,
        memory: SemanticMemory,
        mock_session: AsyncMock,
    ) -> None:
        """Test deprecating non-existent fact returns None."""
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = None
        mock_session.execute.return_value = mock_result

        result = await memory.deprecate_fact(uuid4(), reason="Test")

        assert result is None


class TestSemanticMemoryConflictResolution:
    """Tests for conflict resolution."""

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

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

    @pytest.mark.asyncio
    async def test_resolve_conflict_empty_list(
        self,
        memory: SemanticMemory,
    ) -> None:
        """Test resolving conflict with empty list."""
        result = await memory.resolve_conflict([])

        assert result is None

    @pytest.mark.asyncio
    async def test_resolve_conflict_keeps_highest_confidence(
        self,
        memory: SemanticMemory,
        mock_session: AsyncMock,
    ) -> None:
        """Test that conflict resolution keeps highest confidence fact."""
        fact_low = create_mock_fact_model(confidence=0.5)
        fact_high = create_mock_fact_model(confidence=0.9)

        # Mock finding the facts
        find_result = MagicMock()
        find_result.scalars.return_value.all.return_value = [fact_low, fact_high]

        # Mock deprecation (find + update)
        find_one_result = MagicMock()
        find_one_result.scalar_one_or_none.return_value = fact_low
        update_result = MagicMock()
        update_result.scalar_one_or_none.return_value = fact_low

        mock_session.execute.side_effect = [find_result, find_one_result, update_result]

        result = await memory.resolve_conflict([fact_low.id, fact_high.id])

        assert result is not None
        assert result.confidence == 0.9


class TestSemanticMemoryStats:
    """Tests for statistics 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) -> SemanticMemory:
        """Create a SemanticMemory instance."""
        return SemanticMemory(session=mock_session)

    @pytest.mark.asyncio
    async def test_get_stats_empty(
        self,
        memory: SemanticMemory,
        mock_session: AsyncMock,
    ) -> None:
        """Test getting stats for empty project."""
        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 stats["total_facts"] == 0
        assert stats["avg_confidence"] == 0.0

    @pytest.mark.asyncio
    async def test_count(
        self,
        memory: SemanticMemory,
        mock_session: AsyncMock,
    ) -> None:
        """Test counting facts."""
        facts = [create_mock_fact_model() for _ in range(5)]
        mock_result = MagicMock()
        mock_result.scalars.return_value.all.return_value = facts
        mock_session.execute.return_value = mock_result

        count = await memory.count(uuid4())

        assert count == 5

    @pytest.mark.asyncio
    async def test_delete(
        self,
        memory: SemanticMemory,
        mock_session: AsyncMock,
    ) -> None:
        """Test deleting a fact."""
        existing_mock = create_mock_fact_model()
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = existing_mock
        mock_session.execute.return_value = mock_result
        mock_session.delete = AsyncMock()

        result = await memory.delete(existing_mock.id)

        assert result is True
        mock_session.delete.assert_called_once()

    @pytest.mark.asyncio
    async def test_delete_not_found(
        self,
        memory: SemanticMemory,
        mock_session: AsyncMock,
    ) -> None:
        """Test deleting non-existent fact."""
        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
