"""U3 — SecretsStore Redis 后端单元测试。

覆盖场景：
- Redis 后端 CRUD（set/get/delete/list_keys）
- Redis=None 降级到内存字典
- 加密-存储-读取-解密往返（Redis 后端）
- Redis 连接异常 fail-closed（get_secret 返回 None）
- 多实例共享语义（两个 store 共用同一 Redis mock）

不依赖真实 Redis 实例 — 使用 AsyncMock 模拟 redis.asyncio.Redis。
"""

from __future__ import annotations

import base64
from unittest.mock import AsyncMock, MagicMock

import pytest

from agentkit.channels.secrets import KEY_SIZE, SecretsStore


# ── 辅助函数 ──────────────────────────────────────────────


def _make_master_key() -> bytes:
    """确定性测试 master key（32 字节）。"""
    return b"\x01" * KEY_SIZE


class _FakeRedis:
    """极简 Redis mock — 仅模拟 SecretsStore 用到的方法。

    内部用 dict 存储，支持 set/get/delete/scan_iter。
    scan_iter 返回 async generator（匹配真实 Redis 接口）。
    """

    def __init__(self):
        self._data: dict[str, str] = {}

    async def set(self, key: str, value: str, **kwargs) -> bool:
        self._data[key] = value
        return True

    async def get(self, key: str) -> str | None:
        return self._data.get(key)

    async def delete(self, key: str) -> int:
        if key in self._data:
            del self._data[key]
            return 1
        return 0

    async def scan_iter(self, match: str = "*"):
        import fnmatch

        for k in self._data:
            if fnmatch.fnmatch(k, match):
                yield k


def _make_failing_redis() -> AsyncMock:
    """构造一个所有方法都抛 ConnectionError 的 Redis mock。"""
    redis = AsyncMock()
    redis.get = AsyncMock(side_effect=ConnectionError("redis down"))
    redis.set = AsyncMock(side_effect=ConnectionError("redis down"))
    redis.delete = AsyncMock(side_effect=ConnectionError("redis down"))
    redis.scan_iter = MagicMock(side_effect=ConnectionError("redis down"))
    return redis


@pytest.fixture(autouse=True)
def _clean_env(monkeypatch):
    """确保测试不在生产模式下运行。"""
    monkeypatch.delenv("AGENTKIT_ENV", raising=False)
    monkeypatch.delenv("AGENTKIT_MASTER_KEY", raising=False)


# ── Redis 后端 CRUD ──────────────────────────────────────


class TestRedisBackendCrud:
    """Redis 后端的 set/get/delete/list_keys 操作。"""

    async def test_set_and_get_secret_with_redis(self):
        """Redis 后端：写入后读取返回明文。"""
        redis = _FakeRedis()
        store = SecretsStore(master_key=_make_master_key(), redis=redis)
        await store.set_secret("feishu:app_id", "cli_xxx")
        assert await store.get_secret("feishu:app_id") == "cli_xxx"

    async def test_get_nonexistent_returns_none_with_redis(self):
        """Redis 后端：读取不存在的 key 返回 None。"""
        redis = _FakeRedis()
        store = SecretsStore(master_key=_make_master_key(), redis=redis)
        assert await store.get_secret("missing") is None

    async def test_set_overwrites_existing_with_redis(self):
        """Redis 后端：同名 key 写入覆盖旧值。"""
        redis = _FakeRedis()
        store = SecretsStore(master_key=_make_master_key(), redis=redis)
        await store.set_secret("k", "old")
        await store.set_secret("k", "new")
        assert await store.get_secret("k") == "new"

    async def test_delete_secret_with_redis(self):
        """Redis 后端：删除凭证后不可读取。"""
        redis = _FakeRedis()
        store = SecretsStore(master_key=_make_master_key(), redis=redis)
        await store.set_secret("k", "v")
        assert await store.delete_secret("k") is True
        assert await store.get_secret("k") is None

    async def test_delete_nonexistent_returns_false_with_redis(self):
        """Redis 后端：删除不存在的 key 返回 False。"""
        redis = _FakeRedis()
        store = SecretsStore(master_key=_make_master_key(), redis=redis)
        assert await store.delete_secret("missing") is False

    async def test_list_keys_with_redis(self):
        """Redis 后端：列出全部凭证键。"""
        redis = _FakeRedis()
        store = SecretsStore(master_key=_make_master_key(), redis=redis)
        await store.set_secret("feishu:a", "1")
        await store.set_secret("feishu:b", "2")
        await store.set_secret("dingtalk:c", "3")
        keys = await store.list_keys()
        assert set(keys) == {"feishu:a", "feishu:b", "dingtalk:c"}

    async def test_list_keys_with_prefix_with_redis(self):
        """Redis 后端：按前缀过滤凭证键。"""
        redis = _FakeRedis()
        store = SecretsStore(master_key=_make_master_key(), redis=redis)
        await store.set_secret("feishu:a", "1")
        await store.set_secret("feishu:b", "2")
        await store.set_secret("dingtalk:c", "3")
        keys = await store.list_keys(prefix="feishu:")
        assert set(keys) == {"feishu:a", "feishu:b"}


# ── Redis=None 降级模式 ─────────────────────────────────


class TestFallbackMode:
    """redis=None 时降级到内存字典。"""

    async def test_fallback_set_and_get(self):
        """降级模式：写入后读取返回明文。"""
        store = SecretsStore(master_key=_make_master_key(), redis=None)
        await store.set_secret("k", "v")
        assert await store.get_secret("k") == "v"

    async def test_fallback_delete(self):
        """降级模式：删除凭证后不可读取。"""
        store = SecretsStore(master_key=_make_master_key(), redis=None)
        await store.set_secret("k", "v")
        assert await store.delete_secret("k") is True
        assert await store.get_secret("k") is None

    async def test_fallback_list_keys_with_prefix(self):
        """降级模式：按前缀过滤。"""
        store = SecretsStore(master_key=_make_master_key(), redis=None)
        await store.set_secret("feishu:a", "1")
        await store.set_secret("dingtalk:b", "2")
        keys = await store.list_keys(prefix="feishu:")
        assert keys == ["feishu:a"]


# ── 加密往返 ─────────────────────────────────────────────


class TestRedisEncryptionRoundtrip:
    """Redis 后端的加密-存储-读取-解密往返。"""

    async def test_stored_value_is_encrypted_in_redis(self):
        """Redis 中存储的 value 字段为密文，非明文。"""
        redis = _FakeRedis()
        store = SecretsStore(master_key=_make_master_key(), redis=redis)
        plaintext = "plaintext-token"
        await store.set_secret("k", plaintext)
        # 直接检查 Redis 中的原始值
        raw = redis._data["agentkit:secrets:k"]
        assert plaintext not in raw
        # base64 解码后的密文也不含明文
        import json

        entry = json.loads(raw)
        ciphertext_bytes = base64.b64decode(entry["value"])
        assert plaintext.encode() not in ciphertext_bytes

    async def test_roundtrip_preserves_plaintext(self):
        """加密-存储-读取-解密往返保持明文一致。"""
        redis = _FakeRedis()
        store = SecretsStore(master_key=_make_master_key(), redis=redis)
        plaintext = "feishu-app-secret-12345"
        await store.set_secret("key1", plaintext)
        assert await store.get_secret("key1") == plaintext


# ── 多实例共享语义 ───────────────────────────────────────


class TestMultiInstanceSharing:
    """两个 store 共用同一 Redis → 多 worker 共享语义。"""

    async def test_two_stores_share_state_via_redis(self):
        """worker A set_secret → worker B get_secret 能读到（共享 Redis）。"""
        redis = _FakeRedis()
        store_a = SecretsStore(master_key=_make_master_key(), redis=redis)
        store_b = SecretsStore(master_key=_make_master_key(), redis=redis)

        await store_a.set_secret("shared:key", "value-from-a")
        # worker B 通过同一 Redis 能读到 worker A 写入的凭证
        assert await store_b.get_secret("shared:key") == "value-from-a"

    async def test_delete_from_one_store_visible_to_other(self):
        """worker A delete → worker B get 返回 None。"""
        redis = _FakeRedis()
        store_a = SecretsStore(master_key=_make_master_key(), redis=redis)
        store_b = SecretsStore(master_key=_make_master_key(), redis=redis)

        await store_a.set_secret("k", "v")
        assert await store_b.delete_secret("k") is True
        assert await store_a.get_secret("k") is None


# ── Redis 连接异常 ───────────────────────────────────────


class TestRedisConnectionFailure:
    """Redis 连接失败时的 fail-closed 行为。"""

    async def test_get_secret_raises_on_redis_failure(self):
        """Redis 异常时 get_secret 抛异常（fail-closed，不静默返回 None）。"""
        redis = _make_failing_redis()
        store = SecretsStore(master_key=_make_master_key(), redis=redis)
        with pytest.raises(ConnectionError):
            await store.get_secret("k")

    async def test_set_secret_raises_on_redis_failure(self):
        """Redis 异常时 set_secret 抛异常（写入失败不静默降级）。"""
        redis = _make_failing_redis()
        store = SecretsStore(master_key=_make_master_key(), redis=redis)
        with pytest.raises(ConnectionError):
            await store.set_secret("k", "v")
