"""提现(现金 → 微信零钱)测试。

wxpay 的网络调用(code 换 openid / 发起转账 / 查单)全部 monkeypatch,不真发微信。
现金余额用 SessionLocal 直接 seed(没有"加现金"接口,正常靠金币兑换)。
"""
from __future__ import annotations

from sqlalchemy import select

from app.db.session import SessionLocal
from app.models.user import User
from app.models.wallet import CoinAccount, WechatTransferAuthorization, WithdrawOrder
from app.repositories import wallet as crud_wallet


def _login(client, phone: str) -> str:
    client.post("/api/v1/auth/sms/send", json={"phone": phone})
    r = client.post("/api/v1/auth/sms/login", json={"phone": phone, "code": "123456"})
    assert r.status_code == 200, r.text
    return r.json()["access_token"]


def _auth(token: str) -> dict[str, str]:
    return {"Authorization": f"Bearer {token}"}


def _seed_cash(client, token: str, phone: str, cents: int) -> None:
    """先访问 /account 触发建账户,再用 DB 直接灌现金余额。"""
    client.get("/api/v1/wallet/account", headers=_auth(token))
    db = SessionLocal()
    try:
        user = db.execute(select(User).where(User.phone == phone)).scalar_one()
        acc = db.get(CoinAccount, user.id)
        acc.cash_balance_cents = cents
        db.commit()
    finally:
        db.close()


def _patch_userinfo(monkeypatch, openid="openid_test_abc", nickname="测试昵称", avatar="https://x/y.png"):
    monkeypatch.setattr(
        "app.integrations.wxpay.code_to_userinfo",
        lambda code: {"openid": openid, "nickname": nickname, "avatar_url": avatar, "raw": {}},
    )


def _approve(bill: str) -> None:
    """模拟管理员审核通过(走 repo,等价 admin 端点 approve → execute_withdraw_transfer 发起转账)。"""
    db = SessionLocal()
    try:
        crud_wallet.approve_withdraw(db, bill)
    finally:
        db.close()


def _reject(bill: str, reason: str = "不符合提现规则") -> None:
    """模拟管理员审核拒绝(走 repo,等价 admin 端点 reject → 退款 + rejected)。"""
    db = SessionLocal()
    try:
        crud_wallet.reject_withdraw(db, bill, reason)
    finally:
        db.close()


def test_bind_wechat(client, monkeypatch) -> None:
    _patch_userinfo(monkeypatch)
    token = _login(client, "13800002001")

    r = client.get("/api/v1/wallet/withdraw-info", headers=_auth(token))
    assert r.status_code == 200, r.text
    assert r.json()["wechat_bound"] is False

    r = client.post("/api/v1/wallet/bind-wechat", json={"code": "auth_code_x"}, headers=_auth(token))
    assert r.status_code == 200, r.text
    body = r.json()
    assert body["bound"] is True
    assert body["wechat_nickname"] == "测试昵称"
    assert body["wechat_avatar_url"] == "https://x/y.png"

    r = client.get("/api/v1/wallet/withdraw-info", headers=_auth(token))
    j = r.json()
    assert j["wechat_bound"] is True
    assert j["wechat_nickname"] == "测试昵称"


def test_withdraw_submit_then_approve_success_flow(client, monkeypatch) -> None:
    """提现先进 reviewing(扣款不打款) → 管理员审核通过才发起转账 → 查单 SUCCESS → success。"""
    monkeypatch.setattr("app.integrations.wxpay.code_to_userinfo", lambda code: {"openid": "openid_ok", "nickname": None, "avatar_url": None, "raw": {}})
    monkeypatch.setattr(
        "app.integrations.wxpay.create_transfer",
        lambda openid, amount_fen, out_bill_no, user_name=None: {
            "status_code": 200,
            "data": {"state": "WAIT_USER_CONFIRM", "package_info": "pkg123", "transfer_bill_no": "tb_1"},
        },
    )
    token = _login(client, "13800002002")
    _seed_cash(client, token, "13800002002", 100)
    client.post("/api/v1/wallet/bind-wechat", json={"code": "c"}, headers=_auth(token))

    # 发起提现 50 分 → 进入待审核(reviewing),已扣款但未打款,无 package_info
    r = client.post("/api/v1/wallet/withdraw", json={"amount_cents": 50}, headers=_auth(token))
    assert r.status_code == 200, r.text
    body = r.json()
    assert body["status"] == "reviewing"
    assert body["package_info"] is None
    assert body["cash_balance_cents"] == 50  # 已扣
    bill = body["out_bill_no"]

    # 现金流水有一条 -50 的 withdraw
    r = client.get("/api/v1/wallet/cash-transactions", headers=_auth(token))
    txns = r.json()["items"]
    assert any(t["biz_type"] == "withdraw" and t["amount_cents"] == -50 for t in txns)

    # 管理员审核通过 → 发起微信转账(mock WAIT_USER_CONFIRM,单进 pending)
    _approve(bill)

    # 用户确认到账后查单 → SUCCESS → 归一化 success
    monkeypatch.setattr(
        "app.integrations.wxpay.query_transfer",
        lambda out_bill_no: {"status_code": 200, "data": {"state": "SUCCESS"}},
    )
    r = client.get("/api/v1/wallet/withdraw/status", params={"out_bill_no": bill}, headers=_auth(token))
    assert r.status_code == 200, r.text
    assert r.json()["status"] == "success"

    # 提现单列表有 1 条 success
    r = client.get("/api/v1/wallet/withdraw-orders", headers=_auth(token))
    items = r.json()["items"]
    assert len(items) == 1 and items[0]["status"] == "success"


def test_withdraw_abandoned_confirm_cancels_and_refunds(client, monkeypatch) -> None:
    """用户进了确认页但没确认就回来:查单仍 WAIT_USER_CONFIRM → 撤销微信单 + 退款 + 单置 failed。"""
    monkeypatch.setattr("app.integrations.wxpay.code_to_userinfo", lambda code: {"openid": "openid_wait", "nickname": None, "avatar_url": None, "raw": {}})
    monkeypatch.setattr(
        "app.integrations.wxpay.create_transfer",
        lambda openid, amount_fen, out_bill_no, user_name=None: {
            "status_code": 200,
            "data": {"state": "WAIT_USER_CONFIRM", "package_info": "pkg", "transfer_bill_no": "tb_w"},
        },
    )
    token = _login(client, "13800002006")
    _seed_cash(client, token, "13800002006", 100)
    client.post("/api/v1/wallet/bind-wechat", json={"code": "c"}, headers=_auth(token))

    r = client.post("/api/v1/wallet/withdraw", json={"amount_cents": 50}, headers=_auth(token))
    bill = r.json()["out_bill_no"]
    assert r.json()["status"] == "reviewing"
    assert r.json()["cash_balance_cents"] == 50  # 已扣

    # 管理员审核通过 → 发起转账(mock WAIT_USER_CONFIRM,单进 pending)
    _approve(bill)

    # 回到 app 查单:微信仍是 WAIT_USER_CONFIRM(没确认) + 撤单成功
    monkeypatch.setattr(
        "app.integrations.wxpay.query_transfer",
        lambda out_bill_no: {"status_code": 200, "data": {"state": "WAIT_USER_CONFIRM"}},
    )
    cancel_called = {}
    def _fake_cancel(out_bill_no):
        cancel_called["bill"] = out_bill_no
        return {"status_code": 200, "data": {"state": "CANCELLING"}}
    monkeypatch.setattr("app.integrations.wxpay.cancel_transfer", _fake_cancel)

    r = client.get("/api/v1/wallet/withdraw/status", params={"out_bill_no": bill}, headers=_auth(token))
    assert r.status_code == 200, r.text
    assert r.json()["status"] == "failed"
    assert cancel_called["bill"] == bill  # 确实调了撤单

    # 余额已退回 100
    r = client.get("/api/v1/wallet/account", headers=_auth(token))
    assert r.json()["cash_balance_cents"] == 100
    # 有退款流水
    r = client.get("/api/v1/wallet/cash-transactions", headers=_auth(token))
    assert any(t["biz_type"] == "withdraw_refund" and t["amount_cents"] == 50 for t in r.json()["items"])


def test_withdraw_insufficient_cash(client, monkeypatch) -> None:
    monkeypatch.setattr("app.integrations.wxpay.code_to_userinfo", lambda code: {"openid": "openid_x", "nickname": None, "avatar_url": None, "raw": {}})
    token = _login(client, "13800002003")
    _seed_cash(client, token, "13800002003", 20)
    client.post("/api/v1/wallet/bind-wechat", json={"code": "c"}, headers=_auth(token))

    r = client.post("/api/v1/wallet/withdraw", json={"amount_cents": 50}, headers=_auth(token))
    assert r.status_code == 409, r.text


def test_withdraw_not_bound(client) -> None:
    token = _login(client, "13800002004")
    _seed_cash(client, token, "13800002004", 100)
    r = client.post("/api/v1/wallet/withdraw", json={"amount_cents": 50}, headers=_auth(token))
    assert r.status_code == 400, r.text


def test_withdraw_approve_transfer_fail_refunds(client, monkeypatch) -> None:
    """审核通过发起转账失败(非200) + 查单 NOT_FOUND(未创建) → 自动退款 + 单 failed。"""
    monkeypatch.setattr("app.integrations.wxpay.code_to_userinfo", lambda code: {"openid": "openid_fail", "nickname": None, "avatar_url": None, "raw": {}})
    monkeypatch.setattr(
        "app.integrations.wxpay.create_transfer",
        lambda openid, amount_fen, out_bill_no, user_name=None: {
            "status_code": 400,
            "data": {"code": "PARAM_ERROR", "message": "amount invalid"},
        },
    )
    # #3:非200 也要先查单确认。这里 PARAM_ERROR=没创建,查单返回 NOT_FOUND → 退款安全
    monkeypatch.setattr(
        "app.integrations.wxpay.query_transfer",
        lambda out_bill_no: {"status_code": 404, "data": {"code": "NOT_FOUND"}},
    )
    token = _login(client, "13800002005")
    _seed_cash(client, token, "13800002005", 100)
    client.post("/api/v1/wallet/bind-wechat", json={"code": "c"}, headers=_auth(token))

    # 发起提现 → reviewing(已扣款),此刻不打款不会失败
    r = client.post("/api/v1/wallet/withdraw", json={"amount_cents": 50}, headers=_auth(token))
    assert r.status_code == 200, r.text
    assert r.json()["status"] == "reviewing"
    bill = r.json()["out_bill_no"]

    # 管理员审核通过 → 转账失败 → 自动退款 + 单 failed
    _approve(bill)

    # 余额已退回
    r = client.get("/api/v1/wallet/account", headers=_auth(token))
    assert r.json()["cash_balance_cents"] == 100
    # 有退款流水
    r = client.get("/api/v1/wallet/cash-transactions", headers=_auth(token))
    txns = r.json()["items"]
    assert any(t["biz_type"] == "withdraw_refund" and t["amount_cents"] == 50 for t in txns)
    # 提现单 failed
    r = client.get("/api/v1/wallet/withdraw-orders", headers=_auth(token))
    assert r.json()["items"][0]["status"] == "failed"


def test_approved_withdraw_query_fail_exposes_wechat_reason(client, monkeypatch) -> None:
    """审核通过后微信终态失败时，保存官方失败码对应原因并退回余额。"""
    monkeypatch.setattr(
        "app.integrations.wxpay.code_to_userinfo",
        lambda code: {
            "openid": "openid_query_fail",
            "nickname": None,
            "avatar_url": None,
            "raw": {},
        },
    )
    monkeypatch.setattr(
        "app.integrations.wxpay.create_transfer",
        lambda openid, amount_fen, out_bill_no, user_name=None: {
            "status_code": 200,
            "data": {
                "state": "WAIT_USER_CONFIRM",
                "package_info": "pkg_query_fail",
                "transfer_bill_no": "tb_query_fail",
            },
        },
    )
    token = _login(client, "13800002025")
    _seed_cash(client, token, "13800002025", 100)
    client.post("/api/v1/wallet/bind-wechat", json={"code": "c"}, headers=_auth(token))

    response = client.post(
        "/api/v1/wallet/withdraw",
        json={"amount_cents": 50},
        headers=_auth(token),
    )
    bill = response.json()["out_bill_no"]
    _approve(bill)

    monkeypatch.setattr(
        "app.integrations.wxpay.query_transfer",
        lambda out_bill_no: {
            "status_code": 200,
            "data": {
                "state": "FAIL",
                "fail_reason": "PAYEE_ACCOUNT_ABNORMAL",
                "transfer_bill_no": "tb_query_fail",
            },
        },
    )
    response = client.get(
        "/api/v1/wallet/withdraw/status",
        params={"out_bill_no": bill},
        headers=_auth(token),
    )

    assert response.status_code == 200, response.text
    assert response.json()["status"] == "failed"
    assert response.json()["wechat_state"] == "FAIL"
    assert response.json()["fail_reason"] == (
        "微信转账失败：用户微信账户收款异常（PAYEE_ACCOUNT_ABNORMAL）"
    )
    with SessionLocal() as db:
        order = db.execute(
            select(WithdrawOrder).where(WithdrawOrder.out_bill_no == bill)
        ).scalar_one()
        assert order.transfer_bill_no == "tb_query_fail"
    account = client.get("/api/v1/wallet/account", headers=_auth(token))
    assert account.json()["cash_balance_cents"] == 100


def test_unknown_wechat_fail_reason_code_is_preserved() -> None:
    assert crud_wallet._wechat_terminal_failure_reason(
        {"fail_reason": "NEW_WECHAT_REASON"}, "FAIL"
    ) == "微信转账失败：NEW_WECHAT_REASON"


def test_withdraw_idempotent_same_bill_no(client, monkeypatch) -> None:
    """#2 同 out_bill_no 重试:只转一次,第二次返回同一单,余额只扣一次。"""
    monkeypatch.setattr("app.integrations.wxpay.code_to_userinfo", lambda code: {"openid": "openid_idem", "nickname": None, "avatar_url": None, "raw": {}})
    calls = {"n": 0}
    def _create(openid, amount_fen, out_bill_no, user_name=None):
        calls["n"] += 1
        return {"status_code": 200, "data": {"state": "WAIT_USER_CONFIRM", "package_info": "pkg", "transfer_bill_no": "tb"}}
    monkeypatch.setattr("app.integrations.wxpay.create_transfer", _create)
    # 幂等查单时(pending)会查一次,返回仍在途
    monkeypatch.setattr(
        "app.integrations.wxpay.query_transfer",
        lambda out_bill_no: {"status_code": 200, "data": {"state": "ACCEPTED"}},
    )
    token = _login(client, "13800002007")
    _seed_cash(client, token, "13800002007", 100)
    client.post("/api/v1/wallet/bind-wechat", json={"code": "c"}, headers=_auth(token))

    bill = "idemkey1234567890"
    r1 = client.post("/api/v1/wallet/withdraw", json={"amount_cents": 50, "out_bill_no": bill}, headers=_auth(token))
    assert r1.status_code == 200, r1.text
    r2 = client.post("/api/v1/wallet/withdraw", json={"amount_cents": 50, "out_bill_no": bill}, headers=_auth(token))
    assert r2.status_code == 200, r2.text

    # 提现阶段不打款 → create_transfer 一次都不会被调;两次都返回同一张待审核单
    assert calls["n"] == 0
    assert r1.json()["status"] == "reviewing" and r2.json()["status"] == "reviewing"
    assert r1.json()["out_bill_no"] == bill
    assert r2.json()["out_bill_no"] == bill
    # 余额只扣一次(100-50=50)
    r = client.get("/api/v1/wallet/account", headers=_auth(token))
    assert r.json()["cash_balance_cents"] == 50
    # 只有一条 withdraw 流水
    r = client.get("/api/v1/wallet/cash-transactions", headers=_auth(token))
    assert sum(1 for t in r.json()["items"] if t["biz_type"] == "withdraw") == 1


def test_withdraw_multiple_in_flight_allowed(client, monkeypatch) -> None:
    """取消「同时仅一单」:已有在途(reviewing)时,不同 out_bill_no 可继续提交,两单并存。"""
    _patch_userinfo(monkeypatch, "openid_multi_inflight")
    token = _login(client, "13800002020")
    _seed_cash(client, token, "13800002020", 100)  # 够两笔 0.5
    client.post("/api/v1/wallet/bind-wechat", json={"code": "c"}, headers=_auth(token))

    r1 = client.post(
        "/api/v1/wallet/withdraw",
        json={"amount_cents": 50, "out_bill_no": "billmulti00000001"},
        headers=_auth(token),
    )
    assert r1.status_code == 200, r1.text
    assert r1.json()["status"] == "reviewing"

    r2 = client.post(
        "/api/v1/wallet/withdraw",
        json={"amount_cents": 50, "out_bill_no": "billmulti00000002"},
        headers=_auth(token),
    )
    assert r2.status_code == 200, r2.text
    assert r2.json()["status"] == "reviewing"

    # 两张在途单并存
    r = client.get("/api/v1/wallet/withdraw-orders", headers=_auth(token))
    reviewing = [o for o in r.json()["items"] if o["status"] == "reviewing"]
    assert len(reviewing) == 2, r.text
    # 余额扣两次:100-50-50=0
    r = client.get("/api/v1/wallet/account", headers=_auth(token))
    assert r.json()["cash_balance_cents"] == 0


def test_withdraw_second_blocked_only_by_insufficient_cash(client, monkeypatch) -> None:
    """并行放开后第二笔仅受余额约束:余额不足返 409「现金余额不足」,而非旧的「已有提现」拦截。"""
    _patch_userinfo(monkeypatch, "openid_multi_insuff")
    token = _login(client, "13800002021")
    _seed_cash(client, token, "13800002021", 50)  # 仅够一笔 0.5
    client.post("/api/v1/wallet/bind-wechat", json={"code": "c"}, headers=_auth(token))

    r1 = client.post(
        "/api/v1/wallet/withdraw",
        json={"amount_cents": 50, "out_bill_no": "billinsuff0000001"},
        headers=_auth(token),
    )
    assert r1.status_code == 200, r1.text

    r2 = client.post(
        "/api/v1/wallet/withdraw",
        json={"amount_cents": 50, "out_bill_no": "billinsuff0000002"},
        headers=_auth(token),
    )
    assert r2.status_code == 409, r2.text
    assert "现金余额不足" in r2.json()["detail"]


def test_withdraw_ambiguous_timeout_then_success_no_refund(client, monkeypatch) -> None:
    """#3 转账调用超时(异常),但查单确认已 SUCCESS → 不退款,单置 success。"""
    monkeypatch.setattr("app.integrations.wxpay.code_to_userinfo", lambda code: {"openid": "openid_amb", "nickname": None, "avatar_url": None, "raw": {}})
    def _boom(openid, amount_fen, out_bill_no, user_name=None):
        raise TimeoutError("read timeout")
    monkeypatch.setattr("app.integrations.wxpay.create_transfer", _boom)
    monkeypatch.setattr(
        "app.integrations.wxpay.query_transfer",
        lambda out_bill_no: {"status_code": 200, "data": {"state": "SUCCESS", "transfer_bill_no": "tb_ok"}},
    )
    token = _login(client, "13800002008")
    _seed_cash(client, token, "13800002008", 100)
    client.post("/api/v1/wallet/bind-wechat", json={"code": "c"}, headers=_auth(token))

    r = client.post("/api/v1/wallet/withdraw", json={"amount_cents": 50}, headers=_auth(token))
    assert r.status_code == 200, r.text
    assert r.json()["status"] == "reviewing"
    bill = r.json()["out_bill_no"]

    # 审核通过 → 转账调用超时(异常) → 查单确认 SUCCESS → 不退款,单 success
    _approve(bill)
    r = client.get("/api/v1/wallet/withdraw-orders", headers=_auth(token))
    assert r.json()["items"][0]["status"] == "success"
    # 没退款:余额仍是扣后的 50,且无 withdraw_refund
    r = client.get("/api/v1/wallet/account", headers=_auth(token))
    assert r.json()["cash_balance_cents"] == 50
    r = client.get("/api/v1/wallet/cash-transactions", headers=_auth(token))
    assert not any(t["biz_type"] == "withdraw_refund" for t in r.json()["items"])


def test_bind_rejects_openid_already_bound(client, monkeypatch) -> None:
    """#5 一个微信只能绑一个账号:openid 已属于别的用户 → 409。"""
    shared_openid = "openid_shared_xyz"
    # 先让 A 绑定
    monkeypatch.setattr("app.integrations.wxpay.code_to_userinfo", lambda code: {"openid": shared_openid, "nickname": "A", "avatar_url": None, "raw": {}})
    token_a = _login(client, "13800002009")
    r = client.post("/api/v1/wallet/bind-wechat", json={"code": "ca"}, headers=_auth(token_a))
    assert r.status_code == 200, r.text
    # B 想绑同一个 openid → 409
    token_b = _login(client, "13800002010")
    r = client.post("/api/v1/wallet/bind-wechat", json={"code": "cb"}, headers=_auth(token_b))
    assert r.status_code == 409, r.text


# ===== §10 绑微信时展示身份回填规则 =====

def test_bind_replaces_default_nickname_and_null_avatar(client, monkeypatch) -> None:
    """§10-A: 全默认(昵称=系统默认,头像=null) → 绑微信后两个展示字段都替换为微信值。"""
    monkeypatch.setattr(
        "app.integrations.wxpay.code_to_userinfo",
        lambda code: {"openid": "openid_s10_a", "nickname": "微信昵称A", "avatar_url": "https://x/a.png", "raw": {}},
    )
    token = _login(client, "13800003001")
    r = client.post("/api/v1/wallet/bind-wechat", json={"code": "c"}, headers=_auth(token))
    assert r.status_code == 200, r.text

    r = client.get("/api/v1/auth/me", headers=_auth(token))
    assert r.status_code == 200, r.text
    body = r.json()
    assert body["nickname"] == "微信昵称A"
    assert body["avatar_url"] == "https://x/a.png"


def test_bind_keeps_customized_nickname(client, monkeypatch) -> None:
    """§10-B: 用户已改过昵称 → 绑微信后昵称保留,但 null 头像仍替换为微信头像。"""
    monkeypatch.setattr(
        "app.integrations.wxpay.code_to_userinfo",
        lambda code: {"openid": "openid_s10_b", "nickname": "微信昵称B", "avatar_url": "https://x/b.png", "raw": {}},
    )
    token = _login(client, "13800003002")
    # 先把昵称改成非默认值
    r = client.patch(
        "/api/v1/user/profile", json={"nickname": "我的名字"}, headers=_auth(token)
    )
    assert r.status_code == 200, r.text

    r = client.post("/api/v1/wallet/bind-wechat", json={"code": "c"}, headers=_auth(token))
    assert r.status_code == 200, r.text

    r = client.get("/api/v1/auth/me", headers=_auth(token))
    body = r.json()
    assert body["nickname"] == "我的名字"       # 保留自定义昵称
    assert body["avatar_url"] == "https://x/b.png"  # null 头像被微信头像替换


def test_bind_keeps_customized_avatar(client, monkeypatch) -> None:
    """§10-C: 已有自定义头像 → 绑微信后头像保留,但默认昵称替换为微信昵称。"""
    monkeypatch.setattr(
        "app.integrations.wxpay.code_to_userinfo",
        lambda code: {"openid": "openid_s10_c", "nickname": "微信昵称C", "avatar_url": "https://x/c.png", "raw": {}},
    )
    token = _login(client, "13800003003")
    phone = "13800003003"
    # 直接用 DB 给用户写入自定义头像(保持默认昵称)
    db = SessionLocal()
    try:
        user = db.execute(select(User).where(User.phone == phone)).scalar_one()
        user.avatar_url = "https://custom/av.png"
        db.commit()
    finally:
        db.close()

    r = client.post("/api/v1/wallet/bind-wechat", json={"code": "c"}, headers=_auth(token))
    assert r.status_code == 200, r.text

    r = client.get("/api/v1/auth/me", headers=_auth(token))
    body = r.json()
    assert body["nickname"] == "微信昵称C"           # 默认昵称被替换
    assert body["avatar_url"] == "https://custom/av.png"  # 自定义头像保留


def test_bind_wechat_empty_keeps_default(client, monkeypatch) -> None:
    """§10-D: 微信侧 nickname/avatar 均为 None → 不覆盖,展示字段维持原默认值。"""
    monkeypatch.setattr(
        "app.integrations.wxpay.code_to_userinfo",
        lambda code: {"openid": "openid_s10_d", "nickname": None, "avatar_url": None, "raw": {}},
    )
    token = _login(client, "13800003004")

    r = client.get("/api/v1/auth/me", headers=_auth(token))
    original_nickname = r.json()["nickname"]  # 系统分配的默认昵称
    assert original_nickname.startswith("用户")

    r = client.post("/api/v1/wallet/bind-wechat", json={"code": "c"}, headers=_auth(token))
    assert r.status_code == 200, r.text

    r = client.get("/api/v1/auth/me", headers=_auth(token))
    body = r.json()
    assert body["nickname"] == original_nickname  # 默认昵称不变
    assert body["avatar_url"] is None             # 头像仍为 null


def test_withdraw_reject_refunds(client, monkeypatch) -> None:
    """管理员审核拒绝 → 退回现金 + 单 rejected + 理由写入 fail_reason(用户可见)。"""
    monkeypatch.setattr("app.integrations.wxpay.code_to_userinfo", lambda code: {"openid": "openid_reject", "nickname": None, "avatar_url": None, "raw": {}})
    token = _login(client, "13800002011")
    _seed_cash(client, token, "13800002011", 100)
    client.post("/api/v1/wallet/bind-wechat", json={"code": "c"}, headers=_auth(token))

    # 发起提现 → reviewing(已扣款,余额 50)
    r = client.post("/api/v1/wallet/withdraw", json={"amount_cents": 50}, headers=_auth(token))
    assert r.json()["status"] == "reviewing"
    assert r.json()["cash_balance_cents"] == 50
    bill = r.json()["out_bill_no"]

    # 管理员拒绝 → 退款 + rejected(不调微信,无需 mock 转账)
    _reject(bill, "测试拒绝")

    # 余额退回 100
    r = client.get("/api/v1/wallet/account", headers=_auth(token))
    assert r.json()["cash_balance_cents"] == 100
    # 有退款流水
    r = client.get("/api/v1/wallet/cash-transactions", headers=_auth(token))
    assert any(t["biz_type"] == "withdraw_refund" and t["amount_cents"] == 50 for t in r.json()["items"])
    # 单 rejected + 理由透出
    r = client.get("/api/v1/wallet/withdraw/status", params={"out_bill_no": bill}, headers=_auth(token))
    assert r.json()["status"] == "rejected"
    assert r.json()["fail_reason"] == "测试拒绝"


# ===== §fix 免确认授权 enabled 判定收严:active+authorization_id 非空才算已授权 =====

def _seed_transfer_auth(phone: str, state: str, authorization_id: str | None) -> None:
    """直接在库里写/改该用户的免确认授权记录(绕过微信,用于判定测试)。"""
    db = SessionLocal()
    try:
        user = db.execute(select(User).where(User.phone == phone)).scalar_one()
        auth = db.get(WechatTransferAuthorization, user.id)
        if auth is None:
            auth = WechatTransferAuthorization(
                user_id=user.id, openid=user.wechat_openid or "openid_test_abc",
                out_authorization_no=f"oan_{user.id}",
            )
            db.add(auth)
        auth.state = state
        auth.authorization_id = authorization_id
        db.commit()
    finally:
        db.close()


def test_withdraw_info_auth_enabled_requires_authorization_id(client, monkeypatch) -> None:
    _patch_userinfo(monkeypatch)
    token = _login(client, "13800002051")
    # 触发建号 + 绑定微信(withdraw-info 读 openid)
    client.get("/api/v1/wallet/withdraw-info", headers=_auth(token))
    client.post("/api/v1/wallet/bind-wechat", json={"code": "c1"}, headers=_auth(token))

    # active 但无 authorization_id → 视为未授权
    _seed_transfer_auth("13800002051", "active", None)
    r = client.get("/api/v1/wallet/withdraw-info", headers=_auth(token))
    assert r.status_code == 200, r.text
    assert r.json()["transfer_auth_enabled"] is False

    # active 且有 authorization_id → 已授权
    _seed_transfer_auth("13800002051", "active", "wx_auth_123")
    r = client.get("/api/v1/wallet/withdraw-info", headers=_auth(token))
    assert r.status_code == 200, r.text
    assert r.json()["transfer_auth_enabled"] is True


def test_transfer_auth_status_requires_authorization_id(client, monkeypatch) -> None:
    _patch_userinfo(monkeypatch)
    token = _login(client, "13800002052")
    client.get("/api/v1/wallet/withdraw-info", headers=_auth(token))
    client.post("/api/v1/wallet/bind-wechat", json={"code": "c2"}, headers=_auth(token))

    _seed_transfer_auth("13800002052", "active", None)
    r = client.get("/api/v1/wallet/transfer-auth/status", headers=_auth(token))
    assert r.status_code == 200, r.text
    assert r.json()["state"] == "active"
    assert r.json()["enabled"] is False

    _seed_transfer_auth("13800002052", "active", "wx_auth_456")
    r = client.get("/api/v1/wallet/transfer-auth/status", headers=_auth(token))
    assert r.status_code == 200, r.text
    assert r.json()["enabled"] is True
