"""
DualMemoryProvider 單元測試
=============================

測試案例:
- test_cache_aside_hit: Working Memory 命中
- test_cache_aside_miss_backfill: Working miss，Episodic 命中並回填
- test_dual_write: 雙層寫入
- test_graceful_degradation: 優雅降級 (單層失敗不影響整體)
- test_delete: 雙層刪除
- test_exists: 存在檢查
- test_sync: 手動同步
"""

import pytest
from unittest.mock import patch, AsyncMock, MagicMock, PropertyMock
from pydantic import BaseModel

from lewooogo_data.providers.dual_memory import DualMemoryProvider
from lewooogo_data.providers.redis_memory import RedisMemoryProvider
from lewooogo_data.providers.pg_memory import PgMemoryProvider


class SampleModel(BaseModel):
    """測試用的 Pydantic Model"""
    id: str
    name: str
    value: int = 0


# =============================================================================
# Fixtures
# =============================================================================

@pytest.fixture
def mock_working():
    """Mock Working Memory (Redis)"""
    mock = AsyncMock(spec=RedisMemoryProvider)
    mock._storage = {}

    async def mock_load(key):
        return mock._storage.get(key)

    async def mock_save(key, data, ttl=None):
        mock._storage[key] = data
        return True

    async def mock_delete(key):
        if key in mock._storage:
            del mock._storage[key]
            return True
        return False

    async def mock_exists(key):
        return key in mock._storage

    mock.load = mock_load
    mock.save = mock_save
    mock.delete = mock_delete
    mock.exists = mock_exists

    return mock


@pytest.fixture
def mock_episodic():
    """Mock Episodic Memory (PostgreSQL)"""
    mock = AsyncMock(spec=PgMemoryProvider)
    mock._storage = {}

    async def mock_load(key):
        return mock._storage.get(key)

    async def mock_save(key, data, ttl=None):
        mock._storage[key] = data
        return True

    async def mock_delete(key):
        if key in mock._storage:
            del mock._storage[key]
            return True
        return False

    async def mock_exists(key):
        return key in mock._storage

    mock.load = mock_load
    mock.save = mock_save
    mock.delete = mock_delete
    mock.exists = mock_exists

    return mock


@pytest.fixture
def dual_provider(mock_working, mock_episodic):
    """建立已 mock 的 DualMemoryProvider"""
    provider = DualMemoryProvider(
        model_class=SampleModel,
        key_prefix="test",
        working_ttl=604800,
    )
    # 替換內部 provider
    provider._working = mock_working
    provider._episodic = mock_episodic
    return provider


# =============================================================================
# Cache-Aside 模式測試
# =============================================================================

class TestDualMemoryCacheAside:
    """Cache-Aside 模式測試"""

    @pytest.mark.asyncio
    async def test_cache_aside_hit(self, dual_provider, mock_working, mock_episodic):
        """
        測試 Working Memory 命中

        策略: Working 有資料 → 直接返回，不查 Episodic
        """
        # Arrange
        data = SampleModel(id="hit-1", name="Cache Hit", value=100)
        mock_working._storage["hit-1"] = data

        # Act
        result = await dual_provider.load("hit-1")

        # Assert
        assert result is not None
        assert result.id == "hit-1"
        assert result.name == "Cache Hit"

        # Episodic 不應被查詢 (透過檢查 storage 未被訪問)
        assert "hit-1" not in mock_episodic._storage

    @pytest.mark.asyncio
    async def test_cache_aside_miss_backfill(self, dual_provider, mock_working, mock_episodic):
        """
        測試 Working miss，Episodic 命中並回填

        策略:
        1. Working miss
        2. 查 Episodic，命中
        3. 回填到 Working
        """
        # Arrange - 只在 Episodic 有資料
        data = SampleModel(id="miss-1", name="From PG", value=200)
        mock_episodic._storage["miss-1"] = data

        # Working 是空的
        assert "miss-1" not in mock_working._storage

        # Act
        result = await dual_provider.load("miss-1")

        # Assert
        assert result is not None
        assert result.id == "miss-1"
        assert result.name == "From PG"

        # 驗證回填到 Working
        assert "miss-1" in mock_working._storage
        assert mock_working._storage["miss-1"].name == "From PG"

    @pytest.mark.asyncio
    async def test_cache_aside_total_miss(self, dual_provider, mock_working, mock_episodic):
        """測試兩層都沒有資料"""
        # Act
        result = await dual_provider.load("nonexistent")

        # Assert
        assert result is None


# =============================================================================
# 雙層寫入測試
# =============================================================================

class TestDualMemoryWrite:
    """雙層寫入測試"""

    @pytest.mark.asyncio
    async def test_dual_write(self, dual_provider, mock_working, mock_episodic):
        """
        測試雙層同步寫入

        統帥鐵律: Working + Episodic 必須同步寫入
        """
        # Arrange
        data = SampleModel(id="write-1", name="Dual Write", value=300)

        # Act
        result = await dual_provider.save("write-1", data)

        # Assert
        assert result is True

        # 驗證兩層都有資料
        assert "write-1" in mock_working._storage
        assert "write-1" in mock_episodic._storage

        assert mock_working._storage["write-1"].name == "Dual Write"
        assert mock_episodic._storage["write-1"].name == "Dual Write"


# =============================================================================
# 優雅降級測試
# =============================================================================

class TestDualMemoryGracefulDegradation:
    """優雅降級測試"""

    @pytest.mark.asyncio
    async def test_working_failure_continues(self, dual_provider, mock_working, mock_episodic):
        """
        測試 Working 失敗但 Episodic 成功

        統帥鐵律: 任一層失敗不影響整體
        """
        # Arrange - Working save 會失敗
        async def failing_save(key, data, ttl=None):
            raise Exception("Redis connection failed")

        mock_working.save = failing_save

        data = SampleModel(id="degrade-1", name="Degraded", value=400)

        # Act
        result = await dual_provider.save("degrade-1", data)

        # Assert - 至少 Episodic 成功，整體算成功
        assert result is True
        assert "degrade-1" in mock_episodic._storage

    @pytest.mark.asyncio
    async def test_episodic_failure_continues(self, dual_provider, mock_working, mock_episodic):
        """測試 Episodic 失敗但 Working 成功"""
        # Arrange - Episodic save 會失敗
        async def failing_save(key, data, ttl=None):
            raise Exception("PostgreSQL connection failed")

        mock_episodic.save = failing_save

        data = SampleModel(id="degrade-2", name="Degraded 2", value=500)

        # Act
        result = await dual_provider.save("degrade-2", data)

        # Assert - 至少 Working 成功，整體算成功
        assert result is True
        assert "degrade-2" in mock_working._storage

    @pytest.mark.asyncio
    async def test_both_failure(self, dual_provider, mock_working, mock_episodic):
        """測試兩層都失敗"""
        # Arrange - 兩層都會失敗
        async def failing_save(key, data, ttl=None):
            return False

        mock_working.save = failing_save
        mock_episodic.save = failing_save

        data = SampleModel(id="fail-all", name="All Failed", value=0)

        # Act
        result = await dual_provider.save("fail-all", data)

        # Assert
        assert result is False


# =============================================================================
# 刪除測試
# =============================================================================

class TestDualMemoryDelete:
    """刪除相關測試"""

    @pytest.mark.asyncio
    async def test_delete_both_layers(self, dual_provider, mock_working, mock_episodic):
        """測試雙層刪除"""
        # Arrange - 兩層都有資料
        data = SampleModel(id="del-1", name="To Delete", value=0)
        mock_working._storage["del-1"] = data
        mock_episodic._storage["del-1"] = data

        # Act
        result = await dual_provider.delete("del-1")

        # Assert
        assert result is True
        assert "del-1" not in mock_working._storage
        assert "del-1" not in mock_episodic._storage

    @pytest.mark.asyncio
    async def test_delete_only_working(self, dual_provider, mock_working, mock_episodic):
        """測試只有 Working 有資料時刪除"""
        data = SampleModel(id="del-2", name="Only Working", value=1)
        mock_working._storage["del-2"] = data

        # Act
        result = await dual_provider.delete("del-2")

        # Assert - 至少一層成功
        assert result is True
        assert "del-2" not in mock_working._storage

    @pytest.mark.asyncio
    async def test_delete_nonexistent(self, dual_provider, mock_working, mock_episodic):
        """測試刪除不存在的資料"""
        # Act
        result = await dual_provider.delete("nonexistent")

        # Assert
        assert result is False


# =============================================================================
# 存在檢查測試
# =============================================================================

class TestDualMemoryExists:
    """存在檢查測試"""

    @pytest.mark.asyncio
    async def test_exists_in_working(self, dual_provider, mock_working, mock_episodic):
        """測試只在 Working 有資料"""
        data = SampleModel(id="exist-1", name="In Working", value=1)
        mock_working._storage["exist-1"] = data

        # Act
        result = await dual_provider.exists("exist-1")

        # Assert
        assert result is True

    @pytest.mark.asyncio
    async def test_exists_in_episodic(self, dual_provider, mock_working, mock_episodic):
        """測試只在 Episodic 有資料"""
        data = SampleModel(id="exist-2", name="In Episodic", value=2)
        mock_episodic._storage["exist-2"] = data

        # Act
        result = await dual_provider.exists("exist-2")

        # Assert
        assert result is True

    @pytest.mark.asyncio
    async def test_exists_nowhere(self, dual_provider, mock_working, mock_episodic):
        """測試兩層都沒有"""
        # Act
        result = await dual_provider.exists("nonexistent")

        # Assert
        assert result is False


# =============================================================================
# 同步測試
# =============================================================================

class TestDualMemorySync:
    """同步相關測試"""

    @pytest.mark.asyncio
    async def test_sync_to_working(self, dual_provider, mock_working, mock_episodic):
        """測試從 Episodic 同步到 Working"""
        # Arrange - 只在 Episodic 有資料
        data = SampleModel(id="sync-1", name="To Sync", value=100)
        mock_episodic._storage["sync-1"] = data

        # Act
        result = await dual_provider.sync_to_working("sync-1")

        # Assert
        assert result is True
        assert "sync-1" in mock_working._storage
        assert mock_working._storage["sync-1"].name == "To Sync"

    @pytest.mark.asyncio
    async def test_sync_to_working_nonexistent(self, dual_provider, mock_working, mock_episodic):
        """測試同步不存在的資料到 Working"""
        # Act
        result = await dual_provider.sync_to_working("nonexistent")

        # Assert
        assert result is False

    @pytest.mark.asyncio
    async def test_sync_to_episodic(self, dual_provider, mock_working, mock_episodic):
        """測試從 Working 同步到 Episodic"""
        # Arrange - 只在 Working 有資料
        data = SampleModel(id="sync-2", name="To Persist", value=200)
        mock_working._storage["sync-2"] = data

        # Act
        result = await dual_provider.sync_to_episodic("sync-2")

        # Assert
        assert result is True
        assert "sync-2" in mock_episodic._storage
        assert mock_episodic._storage["sync-2"].name == "To Persist"

    @pytest.mark.asyncio
    async def test_sync_to_episodic_nonexistent(self, dual_provider, mock_working, mock_episodic):
        """測試同步不存在的資料到 Episodic"""
        # Act
        result = await dual_provider.sync_to_episodic("nonexistent")

        # Assert
        assert result is False


# =============================================================================
# Property 測試
# =============================================================================

class TestDualMemoryProperties:
    """Property 測試"""

    def test_working_property(self, dual_provider, mock_working):
        """測試 working property"""
        assert dual_provider.working is mock_working

    def test_episodic_property(self, dual_provider, mock_episodic):
        """測試 episodic property"""
        assert dual_provider.episodic is mock_episodic
