# pyright: reportPrivateUsage=false
"""Unit tests: AsyncResolver and SyncResolver — cached fetching with dedup."""

from unittest.mock import AsyncMock, MagicMock

from seaart._cache import AsyncResolver, SyncResolver


# ── AsyncResolver ───────────────────────────────────────────────────────────


class TestAsyncResolver:
    async def test_cache_miss_calls_fetcher(self) -> None:
        fetcher = AsyncMock(return_value="value_a")
        resolver: AsyncResolver[str, str] = AsyncResolver(fetcher, ttl_seconds=60.0)

        result = await resolver.get("key_a")
        assert result == "value_a"
        fetcher.assert_awaited_once_with("key_a")

    async def test_cache_hit_skips_fetcher(self) -> None:
        fetcher = AsyncMock(return_value="value_a")
        resolver: AsyncResolver[str, str] = AsyncResolver(fetcher, ttl_seconds=60.0)

        await resolver.get("key_a")
        await resolver.get("key_a")
        assert fetcher.await_count == 1

    async def test_different_keys_call_fetcher(self) -> None:
        def echo(k: str) -> str:
            return f"val_{k}"

        fetcher = AsyncMock(side_effect=echo)
        resolver: AsyncResolver[str, str] = AsyncResolver(fetcher, ttl_seconds=60.0)

        assert await resolver.get("a") == "val_a"
        assert await resolver.get("b") == "val_b"
        assert fetcher.await_count == 2

    async def test_invalidate_causes_refetch(self) -> None:
        call_count = 0

        async def fetcher(key: str) -> str:
            nonlocal call_count
            call_count += 1
            return f"val_{call_count}"

        resolver: AsyncResolver[str, str] = AsyncResolver(fetcher, ttl_seconds=60.0)

        assert await resolver.get("a") == "val_1"
        resolver.invalidate("a")
        assert await resolver.get("a") == "val_2"
        assert call_count == 2

    async def test_clear_invalidates_all(self) -> None:
        call_count = 0

        async def fetcher(key: str) -> str:
            nonlocal call_count
            call_count += 1
            return f"val_{call_count}"

        resolver: AsyncResolver[str, str] = AsyncResolver(fetcher, ttl_seconds=60.0)

        await resolver.get("a")
        await resolver.get("b")
        assert call_count == 2

        resolver.clear()
        await resolver.get("a")
        await resolver.get("b")
        assert call_count == 4


# ── SyncResolver ────────────────────────────────────────────────────────────


class TestSyncResolver:
    def test_cache_miss_calls_fetcher(self) -> None:
        fetcher = MagicMock(return_value="value_a")
        resolver: SyncResolver[str, str] = SyncResolver(fetcher, ttl_seconds=60.0)

        result = resolver.get("key_a")
        assert result == "value_a"
        fetcher.assert_called_once_with("key_a")

    def test_cache_hit_skips_fetcher(self) -> None:
        fetcher = MagicMock(return_value="value_a")
        resolver: SyncResolver[str, str] = SyncResolver(fetcher, ttl_seconds=60.0)

        resolver.get("key_a")
        resolver.get("key_a")
        assert fetcher.call_count == 1

    def test_different_keys_call_fetcher(self) -> None:
        def echo(k: str) -> str:
            return f"val_{k}"

        fetcher = MagicMock(side_effect=echo)
        resolver: SyncResolver[str, str] = SyncResolver(fetcher, ttl_seconds=60.0)

        assert resolver.get("a") == "val_a"
        assert resolver.get("b") == "val_b"
        assert fetcher.call_count == 2

    def test_invalidate_causes_refetch(self) -> None:
        call_count = 0

        def fetcher(key: str) -> str:
            nonlocal call_count
            call_count += 1
            return f"val_{call_count}"

        resolver: SyncResolver[str, str] = SyncResolver(fetcher, ttl_seconds=60.0)

        assert resolver.get("a") == "val_1"
        resolver.invalidate("a")
        assert resolver.get("a") == "val_2"
        assert call_count == 2

    def test_clear_invalidates_all(self) -> None:
        call_count = 0

        def fetcher(key: str) -> str:
            nonlocal call_count
            call_count += 1
            return f"val_{call_count}"

        resolver: SyncResolver[str, str] = SyncResolver(fetcher, ttl_seconds=60.0)

        resolver.get("a")
        resolver.get("b")
        assert call_count == 2

        resolver.clear()
        resolver.get("a")
        resolver.get("b")
        assert call_count == 4
