"""福利模块测试:钱包 / 签到 / 一次性任务。

用 sms mock 登录拿 token,再跑各接口闭环。
"""
from __future__ import annotations

import json

from app.core.config import settings
from app.core.rewards import (
    COIN_PER_CENT,
    COIN_PER_YUAN,
    MIN_EXCHANGE_COIN,
    SIGNIN_BOOST_COIN,
    SIGNIN_REWARDS,
    TASK_ENABLE_NOTIFICATION,
    TASK_REWARDS,
)
from app.db.session import SessionLocal
from app.integrations import pangle
from app.repositories import wallet as crud_wallet
from app.repositories.user import get_user_by_phone


def _login(client, phone: str) -> str:
    """sms mock 登录,返回 access_token。"""
    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 _signed_ad_callback(user_id: int, trans_id: str, **params: str) -> dict[str, str]:
    body = {"user_id": str(user_id), "trans_id": trans_id, **params}
    body["sign"] = pangle.build_sign(trans_id, settings.PANGLE_REWARD_SECRET)
    return body


def test_account_auto_created_empty(client) -> None:
    """首次访问账户:自动建空账户,余额全 0。"""
    token = _login(client, "13800001001")
    r = client.get("/api/v1/wallet/account", headers=_auth(token))
    assert r.status_code == 200, r.text
    body = r.json()
    assert body == {
        "coin_balance": 0,
        "cash_balance_cents": 0,
        "invite_cash_balance_cents": 0,    # v2 账户隔离新增(邀请奖励金,与现金隔离)
        "total_coin_earned": 0,
    }


def test_signin_flow(client) -> None:
    """签到状态 → 签到 → 余额到账 → 重复签到 409 → 流水有记录。"""
    token = _login(client, "13800001002")

    # 1. 签到前状态:未签、可签、今天是第 1 档
    r = client.get("/api/v1/signin/status", headers=_auth(token))
    assert r.status_code == 200, r.text
    st = r.json()
    assert st["today_signed"] is False
    assert st["can_claim"] is True
    assert st["today_cycle_day"] == 1
    assert st["today_coin"] == SIGNIN_REWARDS[0]
    assert len(st["steps"]) == len(SIGNIN_REWARDS)
    assert st["steps"][0]["status"] == "today"

    # 2. 执行签到,发第 1 档金币
    r = client.post("/api/v1/signin", headers=_auth(token))
    assert r.status_code == 200, r.text
    res = r.json()
    assert res["cycle_day"] == 1
    assert res["streak"] == 1
    assert res["coin_awarded"] == SIGNIN_REWARDS[0]
    assert res["coin_balance"] == SIGNIN_REWARDS[0]

    # 3. 账户余额到账
    r = client.get("/api/v1/wallet/account", headers=_auth(token))
    assert r.json()["coin_balance"] == SIGNIN_REWARDS[0]

    # 4. 状态变为已签
    r = client.get("/api/v1/signin/status", headers=_auth(token))
    st = r.json()
    assert st["today_signed"] is True
    assert st["can_claim"] is False
    assert st["consecutive_days"] == 1
    assert st["steps"][0]["status"] == "claimed"

    # 5. 重复签到 → 409
    r = client.post("/api/v1/signin", headers=_auth(token))
    assert r.status_code == 409

    # 6. 金币流水有一笔 signin 入账
    r = client.get("/api/v1/wallet/coin-transactions", headers=_auth(token))
    assert r.status_code == 200, r.text
    page = r.json()
    assert page["next_cursor"] is None
    assert len(page["items"]) == 1
    txn = page["items"][0]
    assert txn["amount"] == SIGNIN_REWARDS[0]
    assert txn["biz_type"] == "signin"
    assert txn["balance_after"] == SIGNIN_REWARDS[0]


def test_signin_boost_flow(client) -> None:
    """签到后看广告膨胀 → S2S 固定补发 2000 金币,每天只能膨胀一次。"""
    phone = "13800001011"
    token = _login(client, phone)

    r = client.post("/api/v1/signin/boost", json={}, headers=_auth(token))
    assert r.status_code == 409

    r = client.post("/api/v1/signin", headers=_auth(token))
    assert r.status_code == 200, r.text
    first_coin = r.json()["coin_awarded"]

    with SessionLocal() as db:
        user = get_user_by_phone(db, phone)
        assert user is not None
        uid = user.id

    extra = json.dumps({"reward_scene": "signin_boost", "ad_session_id": "signin-session-1"})
    r = client.get(
        "/api/v1/ad/pangle-callback",
        params=_signed_ad_callback(uid, "signin-boost-trans-1", extra=extra, ecpm="200"),
    )
    assert r.status_code == 200, r.text
    assert r.json() == {"is_verify": True, "reason": 0}

    r = client.post("/api/v1/signin/boost", json={"ad_ref_id": "signin-boost-trans-1"}, headers=_auth(token))
    assert r.status_code == 200, r.text
    body = r.json()
    assert body["coin_awarded"] == SIGNIN_BOOST_COIN
    assert body["coin_balance"] == first_coin + SIGNIN_BOOST_COIN

    r = client.get(
        "/api/v1/ad/pangle-callback",
        params=_signed_ad_callback(uid, "signin-boost-trans-2", extra=extra, ecpm="200"),
    )
    assert r.status_code == 200, r.text

    r = client.get("/api/v1/wallet/coin-transactions", headers=_auth(token))
    types = [t["biz_type"] for t in r.json()["items"]]
    assert "signin" in types
    assert "signin_boost" in types


def test_task_claim_flow(client) -> None:
    """打开消息提醒=可重复任务:每次领取金额减半(750/375/188),claimed 恒 False,余额累加。"""
    token = _login(client, "13800001003")
    base = TASK_REWARDS[TASK_ENABLE_NOTIFICATION]  # 750

    # 1. 任务列表:可重复任务 claimed 恒 False,coin=首次金额(750)
    r = client.get("/api/v1/tasks", headers=_auth(token))
    assert r.status_code == 200, r.text
    items = {it["task_key"]: it for it in r.json()["items"]}
    assert items[TASK_ENABLE_NOTIFICATION]["claimed"] is False
    assert items[TASK_ENABLE_NOTIFICATION]["coin"] == base  # 750

    # 2. 第一次领 → 750,余额 750
    r = client.post(f"/api/v1/tasks/{TASK_ENABLE_NOTIFICATION}/claim", headers=_auth(token))
    assert r.status_code == 200, r.text
    assert r.json()["coin_awarded"] == 750
    assert r.json()["coin_balance"] == 750

    # 3. 列表:下一次金额减半=375,仍 claimed False(可重复)
    r = client.get("/api/v1/tasks", headers=_auth(token))
    items = {it["task_key"]: it for it in r.json()["items"]}
    assert items[TASK_ENABLE_NOTIFICATION]["claimed"] is False
    assert items[TASK_ENABLE_NOTIFICATION]["coin"] == 375

    # 4. 第二次领 → 375(余额 1125),第三次 → 188(四舍五入)
    r = client.post(f"/api/v1/tasks/{TASK_ENABLE_NOTIFICATION}/claim", headers=_auth(token))
    assert r.status_code == 200 and r.json()["coin_awarded"] == 375
    assert r.json()["coin_balance"] == 1125
    r = client.post(f"/api/v1/tasks/{TASK_ENABLE_NOTIFICATION}/claim", headers=_auth(token))
    assert r.status_code == 200 and r.json()["coin_awarded"] == 188

    # 5. 未知任务 → 404
    r = client.post("/api/v1/tasks/no_such_task/claim", headers=_auth(token))
    assert r.status_code == 404


def test_task_notification_claim_capped(client) -> None:
    """可重复任务领满(减半到底)后封顶:不再可领 → 409,列表 claimed 置 True。"""
    from app.core.rewards import notification_max_claims

    token = _login(client, "13800001009")
    base = TASK_REWARDS[TASK_ENABLE_NOTIFICATION]
    cap = notification_max_claims(base)  # 750 → 11

    # 领满 cap 次,每次都应 200(最后几次发 1 金币)
    last_coin = None
    for _ in range(cap):
        r = client.post(f"/api/v1/tasks/{TASK_ENABLE_NOTIFICATION}/claim", headers=_auth(token))
        assert r.status_code == 200, r.text
        last_coin = r.json()["coin_awarded"]
    assert last_coin == 1  # 末次是减半到底的 1

    # 再领 → 409(封顶,不再发放)
    r = client.post(f"/api/v1/tasks/{TASK_ENABLE_NOTIFICATION}/claim", headers=_auth(token))
    assert r.status_code == 409, r.text

    # 列表:领满后 claimed 置 True(客户端据此隐藏)
    r = client.get("/api/v1/tasks", headers=_auth(token))
    items = {it["task_key"]: it for it in r.json()["items"]}
    assert items[TASK_ENABLE_NOTIFICATION]["claimed"] is True


def test_exchange_info(client) -> None:
    token = _login(client, "13800001004")
    r = client.get("/api/v1/wallet/exchange-info", headers=_auth(token))
    assert r.status_code == 200, r.text
    body = r.json()
    assert body["coin_per_yuan"] == COIN_PER_YUAN
    assert body["min_coin"] == MIN_EXCHANGE_COIN
    assert body["step_coin"] == COIN_PER_CENT


def test_exchange_flow(client) -> None:
    """先供款够兑 1 元的金币 → 兑换 1 元 → 金币扣、现金加 → 现金流水有记录。

    兑换下限已降到 1 分(MIN_EXCHANGE_COIN = COIN_PER_CENT = 100), 但本用例验证兑换 1 元,
    直接 grant_coins 注入 COIN_PER_YUAN(= 10000)当种子(够兑 1 元)。
    """
    phone = "13800001005"
    token = _login(client, phone)
    # 供款: 直接注入 1 元额度金币(替代原先靠 notification=10000 领任务供款)
    with SessionLocal() as db:
        user = get_user_by_phone(db, phone)
        assert user is not None
        crud_wallet.grant_coins(db, user.id, COIN_PER_YUAN, biz_type="test_seed", remark="测试供款")
        db.commit()

    # 兑换 10000 金币 → 100 分
    r = client.post(
        "/api/v1/wallet/exchange",
        json={"coin_amount": COIN_PER_YUAN},
        headers=_auth(token),
    )
    assert r.status_code == 200, r.text
    res = r.json()
    assert res["coin_amount"] == COIN_PER_YUAN
    assert res["cash_added_cents"] == 100
    assert res["coin_balance"] == 0
    assert res["cash_balance_cents"] == 100

    # 账户余额同步
    r = client.get("/api/v1/wallet/account", headers=_auth(token))
    assert r.json()["coin_balance"] == 0
    assert r.json()["cash_balance_cents"] == 100

    # 金币流水多了一笔 exchange_out(负)
    r = client.get("/api/v1/wallet/coin-transactions", headers=_auth(token))
    types = [t["biz_type"] for t in r.json()["items"]]
    assert "exchange_out" in types
    out_txn = next(t for t in r.json()["items"] if t["biz_type"] == "exchange_out")
    assert out_txn["amount"] == -COIN_PER_YUAN

    # 现金流水有一笔 exchange_in(正)
    r = client.get("/api/v1/wallet/cash-transactions", headers=_auth(token))
    page = r.json()
    assert len(page["items"]) == 1
    assert page["items"][0]["amount_cents"] == 100
    assert page["items"][0]["biz_type"] == "exchange_in"


def test_exchange_min_floor_one_cent(client) -> None:
    """锁定本次下调的兑换下限:正好兑下限额(MIN_EXCHANGE_COIN=100 金币=1 分)应成功。

    这是本 PR 的核心新能力(下限 10000→100,可兑 1 分起)。test_exchange_flow 兑的是
    1 元(10000),在旧下限下也通过,证明不了新下限;本用例供款并兑换正好等于下限的
    100 金币,断言到账 1 分。若有人把 MIN_EXCHANGE_COIN 改回 10000,100 会因低于下限被
    判 400,本用例随之失败,从而把新下限值钉死。
    """
    phone = "13800001010"
    token = _login(client, phone)
    # 供款: 正好注入一个下限额度的金币(=COIN_PER_CENT=100)
    with SessionLocal() as db:
        user = get_user_by_phone(db, phone)
        assert user is not None
        crud_wallet.grant_coins(
            db, user.id, MIN_EXCHANGE_COIN, biz_type="test_seed", remark="测试供款"
        )
        db.commit()

    # 兑换下限额 100 金币 → 1 分
    r = client.post(
        "/api/v1/wallet/exchange",
        json={"coin_amount": MIN_EXCHANGE_COIN},
        headers=_auth(token),
    )
    assert r.status_code == 200, r.text
    res = r.json()
    assert res["coin_amount"] == MIN_EXCHANGE_COIN
    assert res["cash_added_cents"] == 1  # coins_to_cents(100) == 1
    assert res["coin_balance"] == 0
    assert res["cash_balance_cents"] == 1

    # 金币流水有一笔 exchange_out(=-100)
    r = client.get("/api/v1/wallet/coin-transactions", headers=_auth(token))
    out_txn = next(t for t in r.json()["items"] if t["biz_type"] == "exchange_out")
    assert out_txn["amount"] == -MIN_EXCHANGE_COIN

    # 现金流水有且仅有一笔 exchange_in(=+1 分)
    r = client.get("/api/v1/wallet/cash-transactions", headers=_auth(token))
    cash = r.json()["items"]
    assert len(cash) == 1
    assert cash[0]["amount_cents"] == 1
    assert cash[0]["biz_type"] == "exchange_in"


def test_exchange_insufficient_and_invalid(client) -> None:
    token = _login(client, "13800001006")

    # 余额 0 → 兑换 1 元 → 409 余额不足
    r = client.post(
        "/api/v1/wallet/exchange",
        json={"coin_amount": COIN_PER_YUAN},
        headers=_auth(token),
    )
    assert r.status_code == 409

    # 低于最小额(下限现为 1 分=100;取 MIN_EXCHANGE_COIN-1=99,既低于下限又非整分)→ 400
    r = client.post(
        "/api/v1/wallet/exchange",
        json={"coin_amount": MIN_EXCHANGE_COIN - 1},
        headers=_auth(token),
    )
    assert r.status_code == 400

    # 非整分倍数(带零头)→ 400
    r = client.post(
        "/api/v1/wallet/exchange",
        json={"coin_amount": COIN_PER_YUAN + 1},
        headers=_auth(token),
    )
    assert r.status_code == 400


def _report_savings(client, token: str, n: int, *, original=5000, paid=4200) -> None:
    """上报 n 笔真实比价记录(source='compare'),省额 = original − paid。"""
    for i in range(n):
        client.post(
            "/api/v1/order/report",
            json={
                "client_event_id": f"evt-{token[-6:]}-{i}",
                "platform": "美团外卖",
                "pay_channel": "wechat",
                "compared_price_cents": paid,
                "paid_amount_cents": paid,
                "original_price_cents": original,
                "shop_name": f"测试门店{i}",
                "dishes": ["招牌菜"],
            },
            headers=_auth(token),
        )


def test_savings_summary_and_battle(client) -> None:
    """聚合接口对真实上报记录返回真实算出的数字(demo 兜底已停用)。"""
    token = _login(client, "13800001007")
    _report_savings(client, token, 2)  # 2 笔,各省 5000−4200=800

    r = client.get("/api/v1/savings/summary", headers=_auth(token))
    assert r.status_code == 200, r.text
    s = r.json()
    assert s["order_count"] == 2
    assert s["total_saved_cents"] == 1600
    # 平均每单 = 总额 // 单数
    assert s["avg_saved_cents"] == s["total_saved_cents"] // s["order_count"]

    r = client.get("/api/v1/savings/battle", headers=_auth(token))
    assert r.status_code == 200, r.text
    b = r.json()
    assert b["streak_days"] >= 1  # 今日上报 → 至少连续 1 天
    assert b["week_saved_cents"] >= 0
    assert 0 <= b["beat_percent"] <= 100
    # 完成比价次数与 summary 的 order_count 同源(均 = 真实记录数)
    assert b["compare_count"] == s["order_count"]


def test_savings_empty_for_new_user(client) -> None:
    """新用户无真实上报时全 0(demo 兜底已停用),两次读一致。"""
    token = _login(client, "13800001008")
    first = client.get("/api/v1/savings/summary", headers=_auth(token)).json()
    second = client.get("/api/v1/savings/summary", headers=_auth(token)).json()
    assert first == second
    assert first["order_count"] == 0
    assert first["total_saved_cents"] == 0


def test_savings_records_pagination(client) -> None:
    token = _login(client, "13800001009")
    _report_savings(client, token, 12)  # 12 笔真实记录撑起分页

    # 第一页
    r = client.get("/api/v1/savings/records?limit=5", headers=_auth(token))
    assert r.status_code == 200, r.text
    page1 = r.json()
    assert len(page1["items"]) == 5
    assert page1["next_cursor"] is not None
    # id 倒序
    ids = [it["id"] for it in page1["items"]]
    assert ids == sorted(ids, reverse=True)
    # 第二页接着游标走,不与第一页重叠
    r = client.get(
        f"/api/v1/savings/records?limit=5&cursor={page1['next_cursor']}",
        headers=_auth(token),
    )
    page2 = r.json()
    assert max(it["id"] for it in page2["items"]) < min(ids)


def test_endpoints_require_auth(client) -> None:
    """不带 token 的福利接口统一 401。"""
    assert client.get("/api/v1/wallet/account").status_code == 401
    assert client.get("/api/v1/signin/status").status_code == 401
    assert client.post("/api/v1/signin").status_code == 401
    assert client.get("/api/v1/tasks").status_code == 401
    assert client.get("/api/v1/wallet/cash-transactions").status_code == 401
    assert client.post("/api/v1/wallet/exchange", json={"coin_amount": 10000}).status_code == 401
    assert client.get("/api/v1/savings/summary").status_code == 401
    assert client.get("/api/v1/savings/battle").status_code == 401
    assert client.get("/api/v1/savings/records").status_code == 401
