"""Tests for MemoryCacheClient."""
import asyncio

import pytest
from cache_core.clients.memory import MemoryCacheClient
from cache_core.serializer import SmartSerializer


class TestMemoryKeyValue:
    async def test_set_and_get(self, memory_client: MemoryCacheClient):
        await memory_client.set("key1", "value1")
        assert await memory_client.get("key1") == "value1"

    async def test_get_missing(self, memory_client: MemoryCacheClient):
        assert await memory_client.get("nonexistent") is None

    async def test_delete(self, memory_client: MemoryCacheClient):
        await memory_client.set("key1", "value1")
        assert await memory_client.delete("key1") == 1
        assert await memory_client.get("key1") is None

    async def test_delete_multiple(self, memory_client: MemoryCacheClient):
        await memory_client.set("a", 1)
        await memory_client.set("b", 2)
        assert await memory_client.delete("a", "b", "c") == 2

    async def test_exists(self, memory_client: MemoryCacheClient):
        await memory_client.set("a", 1)
        assert await memory_client.exists("a") == 1
        assert await memory_client.exists("a", "b") == 1

    async def test_ttl_expiry(self, memory_client: MemoryCacheClient):
        await memory_client.set("ephemeral", "gone", ttl=0)  # immediate expiry
        assert await memory_client.get("ephemeral") is None

    async def test_ttl_remaining(self, memory_client: MemoryCacheClient):
        await memory_client.set("keep", "val", ttl=3600)
        remaining = await memory_client.ttl("keep")
        assert 0 <= remaining <= 3600

    async def test_expire(self, memory_client: MemoryCacheClient):
        await memory_client.set("key", "val")
        assert await memory_client.expire("key", 3600) is True
        assert await memory_client.expire("nonexistent", 60) is False

    async def test_incr_decr(self, memory_client: MemoryCacheClient):
        assert await memory_client.incr("counter") == 1
        assert await memory_client.incr("counter", 5) == 6
        assert await memory_client.decr("counter") == 5
        assert await memory_client.decr("counter", 3) == 2


class TestMemorySerialization:
    @pytest.fixture
    async def client(self):
        c = MemoryCacheClient(serializer=SmartSerializer())
        yield c
        await c.flush()

    async def test_complex_value_roundtrip(self, client):
        data = {"name": "test", "nested": {"items": [1, 2, 3]}}
        await client.set("complex", data)
        result = await client.get("complex")
        assert result == data

    async def test_list_of_dicts(self, client):
        await client.rpush("queue", {"id": 1}, {"id": 2})
        items = await client.lrange("queue", 0, -1)
        assert items == [{"id": 1}, {"id": 2}]

    async def test_set_of_dicts(self, client):
        await client.sadd("tags", {"name": "a"}, {"name": "b"}, {"name": "a"})
        members = await client.smembers("tags")
        assert len(members) == 2  # duplicate ignored
        names = {m["name"] for m in members}
        assert names == {"a", "b"}

    async def test_hash_values(self, client):
        await client.hset("users", {
            "1": '{"name":"John","email":"j@example.com"}',
            "2": '{"name":"Jane","email":"jane@example.com"}',
        })
        all_data = await client.hgetall("users")
        assert all_data["1"] == {"name": "John", "email": "j@example.com"}
        assert all_data["2"] == {"name": "Jane", "email": "jane@example.com"}


class TestMemoryDataStructures:
    async def test_hash_ops(self, memory_client: MemoryCacheClient):
        # Use non-numeric strings to avoid SmartSerializer auto-parsing
        await memory_client.hset("h", {"a": "alpha", "b": "beta"})
        assert await memory_client.hget("h", "a") == "alpha"
        assert await memory_client.hget("h", "c") is None
        assert await memory_client.hexists("h", "a") is True
        assert await memory_client.hexists("h", "c") is False
        assert await memory_client.hlen("h") == 2
        assert set(await memory_client.hkeys("h")) == {"a", "b"}
        assert await memory_client.hdel("h", "a") == 1
        assert await memory_client.hget("h", "a") is None

    async def test_list_ops(self, memory_client: MemoryCacheClient):
        await memory_client.rpush("lst", "a", "b", "c")
        assert await memory_client.llen("lst") == 3
        assert await memory_client.lrange("lst", 0, -1) == ["a", "b", "c"]
        assert await memory_client.lrange("lst", 0, 1) == ["a", "b"]
        assert await memory_client.rpop("lst") == "c"
        assert await memory_client.lpop("lst") == "a"
        await memory_client.lpush("lst", "x")
        assert await memory_client.lrange("lst", 0, -1) == ["x", "b"]

    async def test_set_ops(self, memory_client: MemoryCacheClient):
        assert await memory_client.sadd("s", "a", "b", "c") == 3
        assert await memory_client.sadd("s", "a") == 0  # no new
        assert await memory_client.scard("s") == 3
        assert await memory_client.sismember("s", "a") is True
        assert await memory_client.sismember("s", "x") is False
        await memory_client.srem("s", "a")
        assert await memory_client.scard("s") == 2

    async def test_sorted_set_ops(self, memory_client: MemoryCacheClient):
        await memory_client.zadd("zs", {"a": 1.0, "b": 3.0, "c": 2.0})
        assert await memory_client.zcard("zs") == 3
        members = await memory_client.zrange("zs", 0, -1)
        assert members == ["a", "c", "b"]  # by score
        scored = await memory_client.zrange("zs", 0, -1, withscores=True)
        assert scored == [("a", 1.0), ("c", 2.0), ("b", 3.0)]
        await memory_client.zrem("zs", "a")
        assert await memory_client.zcard("zs") == 2

    async def test_keys_and_flush(self, memory_client: MemoryCacheClient):
        await memory_client.set("a:1", 1)
        await memory_client.set("a:2", 2)
        await memory_client.set("b:1", 3)
        assert len(await memory_client.keys("a:*")) == 2
        assert len(await memory_client.keys("b:*")) == 1
        await memory_client.flush()
        assert await memory_client.keys("*") == []


class TestMemoryConcurrency:
    async def test_concurrent_access(self):
        """Multiple concurrent operations should not corrupt state."""
        client = MemoryCacheClient()
        await client.connect()

        async def worker(n: int):
            for i in range(50):
                key = f"worker{n}:{i}"
                await client.set(key, n)
                val = await client.get(key)
                assert val == n, f"worker{n} iteration {i}: got {val}"

        await asyncio.gather(*(worker(i) for i in range(10)))
        # Verify total key count
        all_keys = await client.keys("worker*")
        assert len(all_keys) == 500

        await client.flush()
        await client.disconnect()


class TestMemorySweep:
    """Tests for the background expiry sweep."""

    async def test_sweep_cleans_expired_keys(self):
        """Expired keys should be removed by the background sweep without being accessed."""
        client = MemoryCacheClient()
        client._sweep_interval = 0.1  # sweep every 100ms for fast test

        await client.connect()

        # Set keys with very short TTLs — they'll expire almost immediately
        await client.set("ephemeral:1", "a", ttl=0)
        await client.set("ephemeral:2", "b", ttl=0)
        await client.set("persistent", "keep me")
        await client.hset("hash:short", {"x": "1"})
        await client.rpush("list:short", "item")
        await client.sadd("set:short", "member")
        await client.zadd("zset:short", {"m": 1.0})

        # Set TTLs on data-structure keys (only _ttl dict tracks them)
        client._ttl["hash:short"] = client._now() - 1  # already expired
        client._ttl["list:short"] = client._now() - 1
        client._ttl["set:short"] = client._now() - 1
        client._ttl["zset:short"] = client._now() - 1

        # Wait for at least one sweep cycle
        await asyncio.sleep(0.3)

        # Expired keys should be gone without ever accessing them
        assert await client.get("ephemeral:1") is None
        assert await client.get("ephemeral:2") is None
        assert await client.hgetall("hash:short") == {}
        assert await client.lrange("list:short", 0, -1) == []
        assert await client.smembers("set:short") == []
        assert await client.zrange("zset:short", 0, -1) == []

        # Persistent key still there
        assert await client.get("persistent") == "keep me"

        await client.flush()
        await client.disconnect()

    async def test_sweep_only_removes_expired(self):
        """Sweep should leave non-expired keys alone."""
        client = MemoryCacheClient()
        client._sweep_interval = 0.1

        await client.connect()

        await client.set("live", "alive", ttl=3600)
        await client.set("dead", "gone", ttl=0)

        await asyncio.sleep(0.3)

        assert await client.get("live") == "alive"
        assert await client.get("dead") is None

        await client.flush()
        await client.disconnect()

    async def test_sweep_skips_keys_without_ttl(self):
        """Keys without TTLs should never be swept."""
        client = MemoryCacheClient()
        client._sweep_interval = 0.1

        await client.connect()

        await client.set("forever", "immortal")
        await client.hset("forever_hash", {"a": "1"})

        await asyncio.sleep(0.3)

        assert await client.get("forever") == "immortal"
        assert await client.hgetall("forever_hash") == {"a": "1"}

        await client.flush()
        await client.disconnect()

    async def test_disconnect_stops_sweep(self):
        """After disconnect, the sweep task should be cancelled."""
        client = MemoryCacheClient()
        client._sweep_interval = 0.1

        await client.connect()
        assert client._sweep_task is not None
        assert not client._sweep_task.done()

        await client.disconnect()
        assert client._sweep_task is None  # cleared after cancellation
