# tests/unit/services/memory/working/test_memory.py
"""Unit tests for WorkingMemory class."""

import pytest
import pytest_asyncio

from app.services.memory.exceptions import MemoryNotFoundError
from app.services.memory.types import ScopeContext, ScopeLevel, TaskState
from app.services.memory.working.memory import WorkingMemory
from app.services.memory.working.storage import InMemoryStorage


@pytest.fixture
def scope() -> ScopeContext:
    """Create a test scope."""
    return ScopeContext(
        scope_type=ScopeLevel.SESSION,
        scope_id="test-session-123",
    )


@pytest.fixture
def storage() -> InMemoryStorage:
    """Create a test storage backend."""
    return InMemoryStorage(max_keys=1000)


@pytest_asyncio.fixture
async def memory(scope: ScopeContext, storage: InMemoryStorage) -> WorkingMemory:
    """Create a WorkingMemory instance for testing."""
    wm = WorkingMemory(scope=scope, storage=storage)
    await wm._initialize()
    return wm


class TestWorkingMemoryBasicOperations:
    """Tests for basic key-value operations."""

    @pytest.mark.asyncio
    async def test_set_and_get(self, memory: WorkingMemory) -> None:
        """Test basic set and get."""
        await memory.set("key1", "value1")
        result = await memory.get("key1")
        assert result == "value1"

    @pytest.mark.asyncio
    async def test_get_with_default(self, memory: WorkingMemory) -> None:
        """Test get with default value."""
        result = await memory.get("nonexistent", default="fallback")
        assert result == "fallback"

    @pytest.mark.asyncio
    async def test_delete(self, memory: WorkingMemory) -> None:
        """Test delete operation."""
        await memory.set("key1", "value1")
        result = await memory.delete("key1")
        assert result is True
        assert await memory.exists("key1") is False

    @pytest.mark.asyncio
    async def test_exists(self, memory: WorkingMemory) -> None:
        """Test exists check."""
        await memory.set("key1", "value1")
        assert await memory.exists("key1") is True
        assert await memory.exists("nonexistent") is False

    @pytest.mark.asyncio
    async def test_reserved_key_prefix(self, memory: WorkingMemory) -> None:
        """Test that keys starting with _ are rejected."""
        with pytest.raises(ValueError, match="reserved"):
            await memory.set("_internal", "value")

    @pytest.mark.asyncio
    async def test_cannot_delete_internal_keys(self, memory: WorkingMemory) -> None:
        """Test that internal keys cannot be deleted directly."""
        with pytest.raises(ValueError, match="internal"):
            await memory.delete("_task_state")


class TestWorkingMemoryListAndClear:
    """Tests for list and clear operations."""

    @pytest.mark.asyncio
    async def test_list_keys(self, memory: WorkingMemory) -> None:
        """Test listing keys."""
        await memory.set("key1", "value1")
        await memory.set("key2", "value2")

        keys = await memory.list_keys()
        assert set(keys) == {"key1", "key2"}

    @pytest.mark.asyncio
    async def test_list_keys_excludes_internal(self, memory: WorkingMemory) -> None:
        """Test that list_keys excludes internal keys."""
        await memory.set("user_key", "value")
        # Internal keys exist from initialization
        keys = await memory.list_keys()
        assert all(not k.startswith("_") for k in keys)

    @pytest.mark.asyncio
    async def test_list_keys_with_pattern(self, memory: WorkingMemory) -> None:
        """Test listing keys with pattern."""
        await memory.set("prefix_a", "value1")
        await memory.set("prefix_b", "value2")
        await memory.set("other", "value3")

        keys = await memory.list_keys("prefix_*")
        assert set(keys) == {"prefix_a", "prefix_b"}

    @pytest.mark.asyncio
    async def test_get_all(self, memory: WorkingMemory) -> None:
        """Test getting all key-value pairs."""
        await memory.set("key1", "value1")
        await memory.set("key2", "value2")

        result = await memory.get_all()
        assert result == {"key1": "value1", "key2": "value2"}

    @pytest.mark.asyncio
    async def test_clear_preserves_internal_state(self, memory: WorkingMemory) -> None:
        """Test that clear preserves internal state."""
        # Set some user data
        await memory.set("user_key", "value")

        # Set task state
        state = TaskState(
            task_id="task-1",
            task_type="test",
            description="Test task",
        )
        await memory.set_task_state(state)

        # Clear
        await memory.clear()

        # User data should be gone
        assert await memory.exists("user_key") is False

        # Task state should be preserved
        restored_state = await memory.get_task_state()
        assert restored_state is not None
        assert restored_state.task_id == "task-1"


class TestWorkingMemoryTaskState:
    """Tests for task state operations."""

    @pytest.mark.asyncio
    async def test_set_and_get_task_state(self, memory: WorkingMemory) -> None:
        """Test setting and getting task state."""
        state = TaskState(
            task_id="task-123",
            task_type="code_review",
            description="Review pull request",
            status="in_progress",
            current_step=2,
            total_steps=5,
            progress_percent=40.0,
            context={"pr_id": 456},
        )

        await memory.set_task_state(state)
        result = await memory.get_task_state()

        assert result is not None
        assert result.task_id == "task-123"
        assert result.task_type == "code_review"
        assert result.status == "in_progress"
        assert result.current_step == 2
        assert result.progress_percent == 40.0
        assert result.context == {"pr_id": 456}

    @pytest.mark.asyncio
    async def test_get_task_state_none_when_not_set(
        self, memory: WorkingMemory
    ) -> None:
        """Test that get_task_state returns None when not set."""
        result = await memory.get_task_state()
        assert result is None

    @pytest.mark.asyncio
    async def test_update_task_progress(self, memory: WorkingMemory) -> None:
        """Test updating task progress."""
        state = TaskState(
            task_id="task-123",
            task_type="test",
            description="Test",
            current_step=1,
            progress_percent=10.0,
            status="running",
        )
        await memory.set_task_state(state)

        updated = await memory.update_task_progress(
            current_step=3,
            progress_percent=60.0,
            status="processing",
        )

        assert updated is not None
        assert updated.current_step == 3
        assert updated.progress_percent == 60.0
        assert updated.status == "processing"

    @pytest.mark.asyncio
    async def test_update_task_progress_clamps_percent(
        self, memory: WorkingMemory
    ) -> None:
        """Test that progress percent is clamped to 0-100."""
        state = TaskState(
            task_id="task-123",
            task_type="test",
            description="Test",
        )
        await memory.set_task_state(state)

        updated = await memory.update_task_progress(progress_percent=150.0)
        assert updated is not None
        assert updated.progress_percent == 100.0

        updated = await memory.update_task_progress(progress_percent=-10.0)
        assert updated is not None
        assert updated.progress_percent == 0.0


class TestWorkingMemoryScratchpad:
    """Tests for scratchpad operations."""

    @pytest.mark.asyncio
    async def test_append_and_get_scratchpad(self, memory: WorkingMemory) -> None:
        """Test appending to and getting scratchpad."""
        await memory.append_scratchpad("First note")
        await memory.append_scratchpad("Second note")

        entries = await memory.get_scratchpad()
        assert entries == ["First note", "Second note"]

    @pytest.mark.asyncio
    async def test_get_scratchpad_empty(self, memory: WorkingMemory) -> None:
        """Test getting empty scratchpad."""
        entries = await memory.get_scratchpad()
        assert entries == []

    @pytest.mark.asyncio
    async def test_get_scratchpad_with_timestamps(self, memory: WorkingMemory) -> None:
        """Test getting scratchpad with timestamps."""
        await memory.append_scratchpad("Test note")

        entries = await memory.get_scratchpad_with_timestamps()
        assert len(entries) == 1
        assert entries[0]["content"] == "Test note"
        assert "timestamp" in entries[0]

    @pytest.mark.asyncio
    async def test_clear_scratchpad(self, memory: WorkingMemory) -> None:
        """Test clearing scratchpad."""
        await memory.append_scratchpad("Note 1")
        await memory.append_scratchpad("Note 2")

        count = await memory.clear_scratchpad()
        assert count == 2

        entries = await memory.get_scratchpad()
        assert entries == []


class TestWorkingMemoryCheckpoints:
    """Tests for checkpoint operations."""

    @pytest.mark.asyncio
    async def test_create_checkpoint(self, memory: WorkingMemory) -> None:
        """Test creating a checkpoint."""
        await memory.set("key1", "value1")
        await memory.set("key2", "value2")

        checkpoint_id = await memory.create_checkpoint("Test checkpoint")

        assert checkpoint_id is not None
        assert len(checkpoint_id) == 36  # Full UUID for collision safety

    @pytest.mark.asyncio
    async def test_restore_checkpoint(self, memory: WorkingMemory) -> None:
        """Test restoring from a checkpoint."""
        await memory.set("key1", "original")
        checkpoint_id = await memory.create_checkpoint()

        # Modify state
        await memory.set("key1", "modified")
        await memory.set("key2", "new")

        # Restore
        await memory.restore_checkpoint(checkpoint_id)

        # Check restoration
        assert await memory.get("key1") == "original"
        # key2 didn't exist in checkpoint, so it should be gone
        # But due to checkpoint being restored with clear, it's gone

    @pytest.mark.asyncio
    async def test_restore_nonexistent_checkpoint(self, memory: WorkingMemory) -> None:
        """Test restoring from nonexistent checkpoint raises error."""
        with pytest.raises(MemoryNotFoundError):
            await memory.restore_checkpoint("nonexistent")

    @pytest.mark.asyncio
    async def test_list_checkpoints(self, memory: WorkingMemory) -> None:
        """Test listing checkpoints."""
        cp1 = await memory.create_checkpoint("First")
        cp2 = await memory.create_checkpoint("Second")

        checkpoints = await memory.list_checkpoints()

        assert len(checkpoints) == 2
        ids = [cp["id"] for cp in checkpoints]
        assert cp1 in ids
        assert cp2 in ids

    @pytest.mark.asyncio
    async def test_delete_checkpoint(self, memory: WorkingMemory) -> None:
        """Test deleting a checkpoint."""
        checkpoint_id = await memory.create_checkpoint()

        result = await memory.delete_checkpoint(checkpoint_id)
        assert result is True

        checkpoints = await memory.list_checkpoints()
        assert len(checkpoints) == 0


class TestWorkingMemoryScope:
    """Tests for scope handling."""

    @pytest.mark.asyncio
    async def test_scope_property(
        self, memory: WorkingMemory, scope: ScopeContext
    ) -> None:
        """Test scope property."""
        assert memory.scope == scope

    @pytest.mark.asyncio
    async def test_for_session_factory(self) -> None:
        """Test for_session factory method."""
        # This would normally try Redis and fall back to in-memory
        # In tests, Redis won't be available, so it uses fallback
        wm = await WorkingMemory.for_session(
            session_id="session-abc",
            project_id="project-123",
            agent_instance_id="agent-456",
        )

        assert wm.scope.scope_type == ScopeLevel.SESSION
        assert wm.scope.scope_id == "session-abc"
        assert wm.scope.parent is not None
        assert wm.scope.parent.scope_type == ScopeLevel.AGENT_INSTANCE


class TestWorkingMemoryHealth:
    """Tests for health and lifecycle."""

    @pytest.mark.asyncio
    async def test_is_healthy(self, memory: WorkingMemory) -> None:
        """Test health check."""
        assert await memory.is_healthy() is True

    @pytest.mark.asyncio
    async def test_get_stats(self, memory: WorkingMemory) -> None:
        """Test getting stats."""
        await memory.set("key1", "value1")
        await memory.append_scratchpad("Note")

        state = TaskState(task_id="t1", task_type="test", description="Test")
        await memory.set_task_state(state)

        stats = await memory.get_stats()

        assert stats["scope_type"] == "session"
        assert stats["scope_id"] == "test-session-123"
        assert stats["user_keys"] == 1
        assert stats["scratchpad_entries"] == 1
        assert stats["has_task_state"] is True

    @pytest.mark.asyncio
    async def test_is_using_fallback(self, memory: WorkingMemory) -> None:
        """Test fallback detection."""
        # In-memory storage is always fallback
        assert memory.is_using_fallback is False  # Not set in fixture

    @pytest.mark.asyncio
    async def test_close(self, memory: WorkingMemory) -> None:
        """Test close doesn't error."""
        await memory.close()  # Should not raise
