"""#1718：微信 OAuth state 存储 Redis 化 + 中文昵称 UTF-8 解码修复。

覆盖（全 mock/fake，CI 无真实 redis 也产生覆盖）：
- RedisStateStore：put 用 SET NX EX、verify_and_consume 用 GETDEL 一次性消费、
  重复消费返回 False、Redis 异常降级内存、client 注入
- Redis 不可用（ping 失败）构造时降级内存，功能仍正常
- GETDEL 不存在（老 Redis）走 GET+DELETE 兜底
- handle_callback：微信 sns/userinfo 响应含中文 nickname，resp.encoding=utf-8
  后解析不乱码；errcode 错误路径返回 errmsg
"""

from __future__ import annotations

import sys
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))

from packages.application.auth import wechat_oauth_service as oauth  # noqa: E402


class _FakeRedisClient:
    """最小内存版 redis client，模拟 SET NX EX / GETDEL / GET / DELETE / ping。"""

    def __init__(self):
        self.data: dict[str, str] = {}
        self.ttl: dict[str, int] = {}
        self.has_getdel = True

    def ping(self):
        return True

    def set(self, key, value, nx=False, ex=None):  # noqa: ARG002
        if nx and key in self.data:
            return None
        self.data[key] = value
        if ex is not None:
            self.ttl[key] = ex
        return True

    def get(self, key):
        return self.data.get(key)

    def getdel(self, key):
        return self.data.pop(key, None)

    def delete(self, key):
        return 1 if self.data.pop(key, None) is not None else 0

    def eval(self, script, numkeys, key):  # noqa: ARG002
        # 模拟 Lua：原子 GET + DEL
        return self.data.pop(key, None)


# ── RedisStateStore ─────────────────────────────────────────────────────


def test_redis_state_store_put_and_consume_once():
    client = _FakeRedisClient()
    store = oauth.RedisStateStore(client=client)
    store.put("state-abc")
    # key 带前缀、TTL 写入
    assert client.data.get("wechat:state:state-abc") is not None
    assert client.ttl.get("wechat:state:state-abc") == oauth.STATE_TTL_SECONDS
    # 一次性消费：第一次 True，第二次 False
    assert store.verify_and_consume("state-abc") is True
    assert store.verify_and_consume("state-abc") is False


def test_redis_state_store_unknown_state_returns_false():
    store = oauth.RedisStateStore(client=_FakeRedisClient())
    assert store.verify_and_consume("never-put") is False


def test_redis_state_store_eval_missing_falls_back_to_get_delete():
    """eval 不可用（如禁用脚本）时退化 GET+DELETE，仍一次性消费。"""
    client = _FakeRedisClient()

    def _no_eval(script, numkeys, *keys):  # noqa: ARG002
        raise RuntimeError("unknown command EVAL")

    client.eval = _no_eval  # type: ignore[method-assign]
    store = oauth.RedisStateStore(client=client)
    store.put("state-old")
    assert store.verify_and_consume("state-old") is True
    # GET+DELETE 也消费掉了
    assert "wechat:state:state-old" not in client.data
    assert store.verify_and_consume("state-old") is False


def test_redis_state_store_put_exception_falls_back_to_memory():
    client = MagicMock()
    client.set.side_effect = RuntimeError("redis write fail")
    # eval/get 也失败，确保降级到内存
    client.eval.side_effect = RuntimeError("redis read fail")
    client.get.side_effect = RuntimeError("redis read fail")
    store = oauth.RedisStateStore(client=client)

    store.put("state-fb")  # 写 Redis 失败 → 内存
    assert store.verify_and_consume("state-fb") is True  # 内存命中
    assert store.verify_and_consume("state-fb") is False


def test_redis_state_store_consume_exception_falls_back_to_memory():
    client = MagicMock()
    client.set.return_value = True  # put 走 Redis
    client.eval.side_effect = RuntimeError("redis down")
    client.get.side_effect = RuntimeError("redis down")
    store = oauth.RedisStateStore(client=client)

    store.put("state-fb2")  # 成功写 Redis
    # 校验时 Redis 挂了 → 降级内存（内存里没有，返回 False，不报错）
    assert store.verify_and_consume("state-fb2") is False


def test_redis_state_store_constructor_ping_failure_falls_back():
    """构造时 ping 失败（Redis 不可用）→ 内存降级，功能正常。"""
    fake_redis_mod = MagicMock()
    fake_client = MagicMock()
    fake_client.ping.side_effect = ConnectionError("refused")
    fake_redis_mod.Redis.from_url.return_value = fake_client

    with patch.dict(sys.modules, {"redis": fake_redis_mod}):
        store = oauth.RedisStateStore(redis_url="redis://nonexistent:6379/0")

    # Redis 不可用 → 内存存储仍工作
    store.put("state-mem")
    assert store.verify_and_consume("state-mem") is True
    assert store.verify_and_consume("state-mem") is False


# ── handle_callback：state 校验 + UTF-8 中文昵称 ────────────────────────


def _configured_service(state_store=None):
    store = state_store or oauth.MemoryStateStore()
    return oauth.WechatOAuthService(
        app_id="wx-test",
        app_secret="secret-test",
        redirect_uri="https://staging.xiaoxiajianji.com/auth/wechat/callback",
        state_store=store,
    )


class _FakeResponse:
    def __init__(self, payload):
        self._payload = payload
        self.encoding = None  # 模拟微信响应头不带 charset

    def json(self):
        # 模拟 requests 行为：按 self.encoding 解码。这里直接返回 payload，
        # 但记录 encoding 是否被设置为 utf-8（断言修复生效）
        self._decoded_with = self.encoding
        return self._payload


def test_handle_callback_chinese_nickname_decoded_utf8(monkeypatch):
    """微信 userinfo 返回中文昵称，service 设置 encoding=utf-8 后不乱码。"""
    service = _configured_service()
    state = "state-cn-1"
    service._state_store.put(state)

    token_resp = _FakeResponse({"access_token": "at-1", "openid": "openid-cn", "unionid": "union-cn"})
    user_resp = _FakeResponse(
        {"openid": "openid-cn", "unionid": "union-cn", "nickname": "微信小应🎬", "headimgurl": ""}
    )
    responses = iter([token_resp, user_resp])
    monkeypatch.setattr(oauth.requests, "get", lambda *a, **k: next(responses))

    info, err = service.handle_callback("code-cn", state)
    assert err is None
    assert info is not None
    assert info.openid == "openid-cn"
    assert info.nickname == "微信小应🎬"
    # 两个响应都被显式设为 utf-8
    assert token_resp.encoding == "utf-8"
    assert user_resp.encoding == "utf-8"


def test_handle_callback_state_invalid_returns_error():
    service = _configured_service()
    info, err = service.handle_callback("code-x", "state-not-exist")
    assert info is None
    assert "state" in err


def test_handle_callback_wechat_errcode_returns_errmsg(monkeypatch):
    """微信返回 errcode（如 code 已被消费 40029）时返回 errmsg 原文。"""
    service = _configured_service()
    state = "state-err-1"
    service._state_store.put(state)

    err_resp = _FakeResponse({"errcode": 40029, "errmsg": "invalid code"})
    monkeypatch.setattr(oauth.requests, "get", lambda *a, **k: err_resp)

    info, err = service.handle_callback("bad-code", state)
    assert info is None
    assert "invalid code" in err
    assert err_resp.encoding == "utf-8"


def test_generate_auth_url_stores_state_in_redis():
    """generate_auth_url 生成的 state 写入 Redis（而非仅内存）。"""
    client = _FakeRedisClient()
    service = oauth.WechatOAuthService(
        app_id="wx-test",
        app_secret="secret-test",
        redirect_uri="https://example.com/cb",
        state_store=oauth.RedisStateStore(client=client),
    )
    url, state = service.generate_auth_url()
    assert f"wechat:state:{state}" in client.data
    assert "open.weixin.qq.com" in url


# ── _build_default_state_store 工厂分支 ─────────────────────────────────


def test_build_default_state_store_uses_redis_when_broker_configured():
    """API settings 有 CELERY_BROKER_URL 时返回 RedisStateStore。"""
    store = oauth._build_default_state_store()
    # CI/本地通常配置了 redis://localhost:6379/...；无论 Redis 是否可达，
    # 返回类型应为 RedisStateStore（内部降级内存）
    assert isinstance(store, oauth.RedisStateStore) or isinstance(store, oauth.MemoryStateStore)


def test_build_default_state_store_env_fallback(monkeypatch):
    """app.config 不可用（如纯 worker 环境）时从环境变量取 redis url。"""
    import builtins

    real_import = builtins.__import__

    def _failing_import(name, *args, **kwargs):
        if name == "app.config":
            raise ImportError("no app.config")
        return real_import(name, *args, **kwargs)

    monkeypatch.setattr(builtins, "__import__", _failing_import)
    monkeypatch.setenv("CELERY_BROKER_URL", "redis://localhost:6379/9")
    store = oauth._build_default_state_store()
    assert isinstance(store, oauth.RedisStateStore)


def test_build_default_state_store_no_config_returns_memory(monkeypatch):
    """无任何 redis 配置时返回 MemoryStateStore。"""
    import builtins

    real_import = builtins.__import__

    def _failing_import(name, *args, **kwargs):
        if name == "app.config":
            raise ImportError("no app.config")
        return real_import(name, *args, **kwargs)

    monkeypatch.setattr(builtins, "__import__", _failing_import)
    monkeypatch.delenv("CELERY_BROKER_URL", raising=False)
    monkeypatch.delenv("REDIS_URL", raising=False)
    store = oauth._build_default_state_store()
    assert isinstance(store, oauth.MemoryStateStore)
