# tests/unit/services/memory/working/test_storage.py
"""Unit tests for working memory storage backends."""

import asyncio

import pytest

from app.services.memory.exceptions import MemoryStorageError
from app.services.memory.working.storage import InMemoryStorage


class TestInMemoryStorageBasicOperations:
    """Tests for basic InMemoryStorage operations."""

    @pytest.fixture
    def storage(self) -> InMemoryStorage:
        """Create a fresh storage instance."""
        return InMemoryStorage(max_keys=100)

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

    @pytest.mark.asyncio
    async def test_get_nonexistent_key(self, storage: InMemoryStorage) -> None:
        """Test getting a key that doesn't exist."""
        result = await storage.get("nonexistent")
        assert result is None

    @pytest.mark.asyncio
    async def test_set_overwrites_existing(self, storage: InMemoryStorage) -> None:
        """Test that set overwrites existing values."""
        await storage.set("key1", "original")
        await storage.set("key1", "updated")
        result = await storage.get("key1")
        assert result == "updated"

    @pytest.mark.asyncio
    async def test_delete_existing_key(self, storage: InMemoryStorage) -> None:
        """Test deleting an existing key."""
        await storage.set("key1", "value1")
        result = await storage.delete("key1")
        assert result is True
        assert await storage.get("key1") is None

    @pytest.mark.asyncio
    async def test_delete_nonexistent_key(self, storage: InMemoryStorage) -> None:
        """Test deleting a key that doesn't exist."""
        result = await storage.delete("nonexistent")
        assert result is False

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


class TestInMemoryStorageTTL:
    """Tests for TTL functionality."""

    @pytest.fixture
    def storage(self) -> InMemoryStorage:
        """Create a fresh storage instance."""
        return InMemoryStorage(max_keys=100)

    @pytest.mark.asyncio
    async def test_set_with_ttl(self, storage: InMemoryStorage) -> None:
        """Test that TTL is stored correctly."""
        await storage.set("key1", "value1", ttl_seconds=10)
        # Key should exist immediately
        assert await storage.exists("key1") is True

    @pytest.mark.asyncio
    async def test_ttl_expiration(self, storage: InMemoryStorage) -> None:
        """Test that expired keys return None."""
        await storage.set("key1", "value1", ttl_seconds=0.1)

        # Key exists initially
        assert await storage.get("key1") == "value1"

        # Wait for expiration
        await asyncio.sleep(0.15)

        # Key should be expired
        assert await storage.get("key1") is None
        assert await storage.exists("key1") is False

    @pytest.mark.asyncio
    async def test_remove_ttl_on_update(self, storage: InMemoryStorage) -> None:
        """Test that updating without TTL removes expiration."""
        await storage.set("key1", "value1", ttl_seconds=0.1)
        await storage.set("key1", "value2")  # No TTL

        await asyncio.sleep(0.15)

        # Key should still exist (TTL removed)
        assert await storage.get("key1") == "value2"


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

    @pytest.fixture
    def storage(self) -> InMemoryStorage:
        """Create a fresh storage instance."""
        return InMemoryStorage(max_keys=100)

    @pytest.mark.asyncio
    async def test_list_keys_all(self, storage: InMemoryStorage) -> None:
        """Test listing all keys."""
        await storage.set("key1", "value1")
        await storage.set("key2", "value2")
        await storage.set("other", "value3")

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

    @pytest.mark.asyncio
    async def test_list_keys_with_pattern(self, storage: InMemoryStorage) -> None:
        """Test listing keys with pattern."""
        await storage.set("key1", "value1")
        await storage.set("key2", "value2")
        await storage.set("other", "value3")

        keys = await storage.list_keys("key*")
        assert set(keys) == {"key1", "key2"}

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

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

    @pytest.mark.asyncio
    async def test_clear(self, storage: InMemoryStorage) -> None:
        """Test clearing all keys."""
        await storage.set("key1", "value1")
        await storage.set("key2", "value2")

        count = await storage.clear()
        assert count == 2
        assert await storage.get_all() == {}


class TestInMemoryStorageCapacity:
    """Tests for capacity limits."""

    @pytest.mark.asyncio
    async def test_capacity_limit_exceeded(self) -> None:
        """Test that exceeding capacity raises error."""
        storage = InMemoryStorage(max_keys=2)

        await storage.set("key1", "value1")
        await storage.set("key2", "value2")

        with pytest.raises(MemoryStorageError, match="capacity exceeded"):
            await storage.set("key3", "value3")

    @pytest.mark.asyncio
    async def test_update_existing_key_within_capacity(self) -> None:
        """Test that updating existing key doesn't count against capacity."""
        storage = InMemoryStorage(max_keys=2)

        await storage.set("key1", "value1")
        await storage.set("key2", "value2")
        await storage.set("key1", "updated")  # Should succeed

        assert await storage.get("key1") == "updated"

    @pytest.mark.asyncio
    async def test_expired_keys_freed_for_capacity(self) -> None:
        """Test that expired keys are cleaned up for capacity."""
        storage = InMemoryStorage(max_keys=2)

        await storage.set("key1", "value1", ttl_seconds=0.1)
        await storage.set("key2", "value2")

        await asyncio.sleep(0.15)

        # Should succeed because key1 is expired and will be cleaned
        await storage.set("key3", "value3")
        assert await storage.get("key3") == "value3"


class TestInMemoryStorageDataTypes:
    """Tests for different data types."""

    @pytest.fixture
    def storage(self) -> InMemoryStorage:
        """Create a fresh storage instance."""
        return InMemoryStorage(max_keys=100)

    @pytest.mark.asyncio
    async def test_store_dict(self, storage: InMemoryStorage) -> None:
        """Test storing dict values."""
        data = {"nested": {"key": "value"}, "list": [1, 2, 3]}
        await storage.set("dict_key", data)
        result = await storage.get("dict_key")
        assert result == data

    @pytest.mark.asyncio
    async def test_store_list(self, storage: InMemoryStorage) -> None:
        """Test storing list values."""
        data = [1, 2, {"nested": "dict"}]
        await storage.set("list_key", data)
        result = await storage.get("list_key")
        assert result == data

    @pytest.mark.asyncio
    async def test_store_numbers(self, storage: InMemoryStorage) -> None:
        """Test storing numeric values."""
        await storage.set("int_key", 42)
        await storage.set("float_key", 3.14)

        assert await storage.get("int_key") == 42
        assert await storage.get("float_key") == 3.14

    @pytest.mark.asyncio
    async def test_store_boolean(self, storage: InMemoryStorage) -> None:
        """Test storing boolean values."""
        await storage.set("true_key", True)
        await storage.set("false_key", False)

        assert await storage.get("true_key") is True
        assert await storage.get("false_key") is False

    @pytest.mark.asyncio
    async def test_store_none(self, storage: InMemoryStorage) -> None:
        """Test storing None value."""
        await storage.set("none_key", None)
        # Note: None is stored, but get returns None for both missing and None values
        # Use exists to distinguish
        assert await storage.exists("none_key") is True


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

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

    @pytest.mark.asyncio
    async def test_close(self) -> None:
        """Test close is no-op but doesn't error."""
        storage = InMemoryStorage()
        await storage.close()  # Should not raise


class TestInMemoryStorageConcurrency:
    """Tests for concurrent access."""

    @pytest.mark.asyncio
    async def test_concurrent_writes(self) -> None:
        """Test concurrent write operations don't corrupt data."""
        storage = InMemoryStorage(max_keys=1000)

        async def write_batch(prefix: str, count: int) -> None:
            for i in range(count):
                await storage.set(f"{prefix}_{i}", f"value_{i}")

        # Run concurrent writes
        await asyncio.gather(
            write_batch("a", 100),
            write_batch("b", 100),
            write_batch("c", 100),
        )

        # Verify all writes succeeded
        keys = await storage.list_keys()
        assert len(keys) == 300

    @pytest.mark.asyncio
    async def test_concurrent_read_write(self) -> None:
        """Test concurrent read and write operations."""
        storage = InMemoryStorage()
        await storage.set("key", 0)

        async def increment() -> None:
            for _ in range(100):
                val = await storage.get("key") or 0
                await storage.set("key", val + 1)

        # Run concurrent increments
        await asyncio.gather(
            increment(),
            increment(),
        )

        # Final value depends on interleaving
        # Just verify we don't crash and value is positive
        result = await storage.get("key")
        assert result > 0
