"""Tests for memory base classes and implementations."""

import json
import os
import tempfile
from datetime import datetime

import pytest

from agentbyte.memory import (
    FileMemory,
    FileMemoryConfig,
    ListMemory,
    ListMemoryConfig,
    MemoryContent,
    MemoryQueryResult,
)


class TestMemoryContent:
    """Test MemoryContent data model."""

    def test_create_memory_content_with_string(self):
        """Test creating memory content with string data."""
        content = MemoryContent(content="User prefers dark mode")
        assert content.content == "User prefers dark mode"
        assert content.mime_type == "text/plain"
        assert content.metadata == {}
        assert isinstance(content.timestamp, datetime)

    def test_create_memory_content_with_dict(self):
        """Test creating memory content with dictionary data."""
        data = {"preference": "dark_mode", "language": "en"}
        content = MemoryContent(content=data, metadata={"category": "settings"})
        assert content.content == data
        assert content.metadata["category"] == "settings"

    def test_memory_content_is_frozen(self):
        """Test that MemoryContent is immutable."""
        content = MemoryContent(content="test")
        with pytest.raises(Exception):  # Pydantic raises ValidationError
            content.content = "modified"


class TestMemoryQueryResult:
    """Test MemoryQueryResult data model."""

    def test_create_empty_result(self):
        """Test creating empty query result."""
        result = MemoryQueryResult()
        assert result.results == []

    def test_create_result_with_memories(self):
        """Test creating query result with memories."""
        content1 = MemoryContent(content="memory 1")
        content2 = MemoryContent(content="memory 2")
        result = MemoryQueryResult(results=[content1, content2])
        assert len(result.results) == 2
        assert result.results[0].content == "memory 1"

    def test_query_result_is_frozen(self):
        """Test that MemoryQueryResult is immutable."""
        result = MemoryQueryResult()
        with pytest.raises(Exception):
            result.results = [MemoryContent(content="test")]


class TestListMemory:
    """Test ListMemory in-memory implementation."""

    @pytest.mark.asyncio
    async def test_create_list_memory(self):
        """Test creating ListMemory instance."""
        memory = ListMemory(max_memories=100)
        assert memory.max_memories == 100
        assert memory.memories == []

    @pytest.mark.asyncio
    async def test_add_memory(self):
        """Test adding memory to list."""
        memory = ListMemory()
        content = MemoryContent(content="test memory")
        await memory.add(content)
        assert len(memory.memories) == 1
        assert memory.memories[0].content == "test memory"

    @pytest.mark.asyncio
    async def test_add_multiple_memories(self):
        """Test adding multiple memories."""
        memory = ListMemory()
        for i in range(5):
            await memory.add(MemoryContent(content=f"memory {i}"))
        assert len(memory.memories) == 5

    @pytest.mark.asyncio
    async def test_fifo_eviction_when_exceeding_capacity(self):
        """Test FIFO eviction when max_memories exceeded."""
        memory = ListMemory(max_memories=3)
        for i in range(5):
            await memory.add(MemoryContent(content=f"memory {i}"))

        # Should only keep last 3 memories
        assert len(memory.memories) == 3
        assert memory.memories[0].content == "memory 2"
        assert memory.memories[1].content == "memory 3"
        assert memory.memories[2].content == "memory 4"

    @pytest.mark.asyncio
    async def test_query_with_matching_text(self):
        """Test querying memories with text match."""
        memory = ListMemory()
        await memory.add(MemoryContent(content="User prefers dark mode"))
        await memory.add(MemoryContent(content="Project deadline is Friday"))
        await memory.add(MemoryContent(content="Dark theme is enabled"))

        result = await memory.query("dark", limit=10)
        assert len(result.results) == 2
        # Most recent first
        assert "Dark theme" in result.results[0].content
        assert "dark mode" in result.results[1].content

    @pytest.mark.asyncio
    async def test_query_with_limit(self):
        """Test query limit parameter."""
        memory = ListMemory()
        for i in range(10):
            await memory.add(MemoryContent(content=f"test item {i}"))

        result = await memory.query("test", limit=3)
        assert len(result.results) == 3

    @pytest.mark.asyncio
    async def test_query_with_no_matches(self):
        """Test query with no matching results."""
        memory = ListMemory()
        await memory.add(MemoryContent(content="hello world"))

        result = await memory.query("nonexistent", limit=10)
        assert len(result.results) == 0

    @pytest.mark.asyncio
    async def test_query_with_dict_content(self):
        """Test querying memories with dictionary content."""
        memory = ListMemory()
        await memory.add(MemoryContent(content={"key": "value", "name": "test"}))
        await memory.add(MemoryContent(content={"key": "other"}))

        result = await memory.query("test", limit=10)
        assert len(result.results) == 1

    @pytest.mark.asyncio
    async def test_get_context_returns_recent_memories(self):
        """Test get_context returns most recent memories."""
        memory = ListMemory()
        for i in range(10):
            await memory.add(MemoryContent(content=f"memory {i}"))

        context = await memory.get_context(max_items=3)
        assert len(context.results) == 3
        assert context.results[0].content == "memory 7"
        assert context.results[1].content == "memory 8"
        assert context.results[2].content == "memory 9"

    @pytest.mark.asyncio
    async def test_get_context_with_empty_memory(self):
        """Test get_context with no memories."""
        memory = ListMemory()
        context = await memory.get_context(max_items=10)
        assert len(context.results) == 0

    @pytest.mark.asyncio
    async def test_clear_memories(self):
        """Test clearing all memories."""
        memory = ListMemory()
        await memory.add(MemoryContent(content="test 1"))
        await memory.add(MemoryContent(content="test 2"))
        assert len(memory.memories) == 2

        await memory.clear()
        assert len(memory.memories) == 0

    @pytest.mark.asyncio
    async def test_get_stats(self):
        """Test memory statistics."""
        memory = ListMemory(max_memories=100)
        await memory.add(MemoryContent(content="test"))

        stats = await memory.get_stats()
        assert stats["max_memories"] == 100
        assert stats["current_memories"] == 1
        assert stats["is_persistent"] is False
        assert stats["implementation"] == "ListMemory"

    @pytest.mark.asyncio
    async def test_list_memory_to_from_config_round_trip(self):
        """Test ListMemory serialization round trip."""
        memory = ListMemory(max_memories=7)
        config = memory._to_config()

        assert isinstance(config, ListMemoryConfig)
        assert config.component_type == "ListMemory"
        assert config.max_memories == 7

        restored = ListMemory._from_config(config)
        assert isinstance(restored, ListMemory)
        assert restored.max_memories == 7


class TestFileMemory:
    """Test FileMemory persistent implementation."""

    @pytest.mark.asyncio
    async def test_create_file_memory(self):
        """Test creating FileMemory instance."""
        with tempfile.NamedTemporaryFile(delete=False, suffix=".json") as tmp:
            tmp_path = tmp.name

        try:
            memory = FileMemory(file_path=tmp_path, max_memories=100)
            assert memory.max_memories == 100
            assert memory.file_path == tmp_path
            assert memory.memories == []
        finally:
            if os.path.exists(tmp_path):
                os.remove(tmp_path)

    @pytest.mark.asyncio
    async def test_add_memory_persists_to_file(self):
        """Test that adding memory persists to file."""
        with tempfile.NamedTemporaryFile(delete=False, suffix=".json") as tmp:
            tmp_path = tmp.name

        try:
            memory = FileMemory(file_path=tmp_path)
            await memory.add(MemoryContent(content="persistent memory"))

            # Verify file was created and contains data
            assert os.path.exists(tmp_path)
            with open(tmp_path, "r") as f:
                data = json.load(f)
                assert len(data) == 1
                assert data[0]["content"] == "persistent memory"
        finally:
            if os.path.exists(tmp_path):
                os.remove(tmp_path)

    @pytest.mark.asyncio
    async def test_load_memories_from_existing_file(self):
        """Test loading memories from existing file."""
        with tempfile.NamedTemporaryFile(delete=False, suffix=".json") as tmp:
            tmp_path = tmp.name

        try:
            # Create first instance and add memories
            memory1 = FileMemory(file_path=tmp_path)
            await memory1.add(MemoryContent(content="memory 1"))
            await memory1.add(MemoryContent(content="memory 2"))

            # Create second instance - should load existing memories
            memory2 = FileMemory(file_path=tmp_path)
            assert len(memory2.memories) == 2
            assert memory2.memories[0].content == "memory 1"
            assert memory2.memories[1].content == "memory 2"
        finally:
            if os.path.exists(tmp_path):
                os.remove(tmp_path)

    @pytest.mark.asyncio
    async def test_fifo_eviction_persists(self):
        """Test that FIFO eviction is persisted to file."""
        with tempfile.NamedTemporaryFile(delete=False, suffix=".json") as tmp:
            tmp_path = tmp.name

        try:
            memory = FileMemory(file_path=tmp_path, max_memories=3)
            for i in range(5):
                await memory.add(MemoryContent(content=f"memory {i}"))

            # Reload from file
            memory2 = FileMemory(file_path=tmp_path, max_memories=3)
            assert len(memory2.memories) == 3
            assert memory2.memories[0].content == "memory 2"
        finally:
            if os.path.exists(tmp_path):
                os.remove(tmp_path)

    @pytest.mark.asyncio
    async def test_query_persisted_memories(self):
        """Test querying persisted memories."""
        with tempfile.NamedTemporaryFile(delete=False, suffix=".json") as tmp:
            tmp_path = tmp.name

        try:
            memory = FileMemory(file_path=tmp_path)
            await memory.add(MemoryContent(content="User prefers dark mode"))
            await memory.add(MemoryContent(content="Meeting tomorrow at 3pm"))

            result = await memory.query("dark", limit=10)
            assert len(result.results) == 1
            assert "dark mode" in result.results[0].content
        finally:
            if os.path.exists(tmp_path):
                os.remove(tmp_path)

    @pytest.mark.asyncio
    async def test_get_context_from_persisted_memories(self):
        """Test get_context with persisted memories."""
        with tempfile.NamedTemporaryFile(delete=False, suffix=".json") as tmp:
            tmp_path = tmp.name

        try:
            memory = FileMemory(file_path=tmp_path)
            for i in range(5):
                await memory.add(MemoryContent(content=f"memory {i}"))

            context = await memory.get_context(max_items=2)
            assert len(context.results) == 2
            assert context.results[0].content == "memory 3"
            assert context.results[1].content == "memory 4"
        finally:
            if os.path.exists(tmp_path):
                os.remove(tmp_path)

    @pytest.mark.asyncio
    async def test_clear_removes_file(self):
        """Test that clear removes the file."""
        with tempfile.NamedTemporaryFile(delete=False, suffix=".json") as tmp:
            tmp_path = tmp.name

        try:
            memory = FileMemory(file_path=tmp_path)
            await memory.add(MemoryContent(content="test"))
            assert os.path.exists(tmp_path)

            await memory.clear()
            assert len(memory.memories) == 0
            assert not os.path.exists(tmp_path)
        finally:
            if os.path.exists(tmp_path):
                os.remove(tmp_path)

    @pytest.mark.asyncio
    async def test_corrupted_file_starts_fresh(self):
        """Test that corrupted file is handled gracefully."""
        with tempfile.NamedTemporaryFile(delete=False, suffix=".json", mode="w") as tmp:
            tmp.write("invalid json {{{")
            tmp_path = tmp.name

        try:
            memory = FileMemory(file_path=tmp_path)
            # Should start with empty memories instead of crashing
            assert memory.memories == []
        finally:
            if os.path.exists(tmp_path):
                os.remove(tmp_path)

    @pytest.mark.asyncio
    async def test_get_stats(self):
        """Test file memory statistics."""
        with tempfile.NamedTemporaryFile(delete=False, suffix=".json") as tmp:
            tmp_path = tmp.name

        try:
            memory = FileMemory(file_path=tmp_path, max_memories=50)
            await memory.add(MemoryContent(content="test"))

            stats = await memory.get_stats()
            assert stats["max_memories"] == 50
            assert stats["current_memories"] == 1
            assert stats["is_persistent"] is True
            assert stats["file_path"] == tmp_path
            assert stats["implementation"] == "FileMemory"
        finally:
            if os.path.exists(tmp_path):
                os.remove(tmp_path)

    @pytest.mark.asyncio
    async def test_file_in_nested_directory(self):
        """Test creating file in nested directory structure."""
        with tempfile.TemporaryDirectory() as tmpdir:
            tmp_path = os.path.join(tmpdir, "nested", "dir", "memory.json")

            memory = FileMemory(file_path=tmp_path)
            await memory.add(MemoryContent(content="test in nested dir"))

            # Verify nested directories were created
            assert os.path.exists(tmp_path)
            with open(tmp_path, "r") as f:
                data = json.load(f)
                assert data[0]["content"] == "test in nested dir"

    @pytest.mark.asyncio
    async def test_file_memory_to_from_config_round_trip(self):
        """Test FileMemory serialization round trip."""
        with tempfile.NamedTemporaryFile(delete=False, suffix=".json") as tmp:
            tmp_path = tmp.name

        try:
            memory = FileMemory(file_path=tmp_path, max_memories=11)
            config = memory._to_config()

            assert isinstance(config, FileMemoryConfig)
            assert config.component_type == "FileMemory"
            assert config.file_path == tmp_path
            assert config.max_memories == 11

            restored = FileMemory._from_config(config)
            assert isinstance(restored, FileMemory)
            assert restored.file_path == tmp_path
            assert restored.max_memories == 11
        finally:
            if os.path.exists(tmp_path):
                os.remove(tmp_path)
