"""好友邀请测试:邀请码、绑定(双方不发钱)、幂等、自邀/无效码屏蔽、指纹兜底归因。

用 sms mock 登录拿 token(同 test_welfare),再跑邀请闭环。
"""
from __future__ import annotations

from datetime import datetime, timedelta, timezone

from sqlalchemy import select

from app.core.rewards import (
    INVITE_FP_WINDOW_DAYS,
    INVITE_NEW_USER_WINDOW_HOURS,
)
from app.db.session import SessionLocal
from app.models.invite_fingerprint import InviteFingerprint
from app.repositories.user import get_user_by_phone


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 _coin_balance(client, token: str) -> int:
    r = client.get("/api/v1/wallet/account", headers=_auth(token))
    assert r.status_code == 200, r.text
    return r.json()["coin_balance"]


def _my_code(client, token: str) -> str:
    r = client.get("/api/v1/invite/me", headers=_auth(token))
    assert r.status_code == 200, r.text
    return r.json()["invite_code"]


def test_invite_me_returns_stable_code(client) -> None:
    """/invite/me 返回邀请码 + 分享链接;同一用户多次取码稳定。"""
    token = _login(client, "13800002001")
    r = client.get("/api/v1/invite/me", headers=_auth(token))
    assert r.status_code == 200, r.text
    body = r.json()
    assert body["invite_code"]                      # 非空
    assert f"ref={body['invite_code']}" in body["share_url"]
    assert body["invited_count"] == 0
    assert body["coins_earned"] == 0
    # 再取一次,码不变(懒生成 + 持久化)
    assert _my_code(client, token) == body["invite_code"]


def test_bind_flow_no_coins_either_side(client) -> None:
    """v3:B 用 A 的码绑定 → 双方都不发钱(被邀请人无奖励、邀请人改比价后发现金)。"""
    a = _login(client, "13800002002")
    b = _login(client, "13800002003")
    a_code = _my_code(client, a)

    assert _coin_balance(client, a) == 0
    assert _coin_balance(client, b) == 0

    r = client.post("/api/v1/invite/bind", json={"invite_code": a_code}, headers=_auth(b))
    assert r.status_code == 200, r.text
    res = r.json()
    assert res["status"] == "success"
    assert res["coins_awarded"] == 0

    # 绑定后双方金币都不增(邀请人收益改走"好友比价发 2 元邀请奖励金")
    assert _coin_balance(client, b) == 0
    assert _coin_balance(client, a) == 0

    # A 的战绩:已邀 1 人;金币口径收益恒 0(邀请人收益走邀请奖励金,见 try_reward_on_compare)
    r = client.get("/api/v1/invite/me", headers=_auth(a))
    assert r.json()["invited_count"] == 1
    assert r.json()["coins_earned"] == 0


def test_bind_idempotent_no_double_reward(client) -> None:
    """同一被邀请人二次绑定 → already_bound,不重复发奖。"""
    a = _login(client, "13800002004")
    b = _login(client, "13800002005")
    a_code = _my_code(client, a)

    r1 = client.post("/api/v1/invite/bind", json={"invite_code": a_code}, headers=_auth(b))
    assert r1.json()["status"] == "success"
    bal_b = _coin_balance(client, b)
    bal_a = _coin_balance(client, a)

    # 再绑一次(甚至换个码也不行,已被绑过)
    c = _login(client, "13800002006")
    c_code = _my_code(client, c)
    r2 = client.post("/api/v1/invite/bind", json={"invite_code": c_code}, headers=_auth(b))
    assert r2.json()["status"] == "already_bound"
    assert r2.json()["coins_awarded"] == 0

    # 余额没变(没二次发奖),C 也没拿到邀请奖励
    assert _coin_balance(client, b) == bal_b
    assert _coin_balance(client, a) == bal_a
    assert _coin_balance(client, c) == 0


def test_self_invite_blocked(client) -> None:
    """填自己的邀请码 → self_invite,不发奖。"""
    a = _login(client, "13800002007")
    a_code = _my_code(client, a)
    r = client.post("/api/v1/invite/bind", json={"invite_code": a_code}, headers=_auth(a))
    assert r.status_code == 200, r.text
    assert r.json()["status"] == "self_invite"
    assert _coin_balance(client, a) == 0


def test_invalid_code(client) -> None:
    """无效邀请码 → invalid_code,不发奖。"""
    b = _login(client, "13800002008")
    r = client.post("/api/v1/invite/bind", json={"invite_code": "ZZZZZZ"}, headers=_auth(b))
    assert r.status_code == 200, r.text
    assert r.json()["status"] == "invalid_code"
    assert _coin_balance(client, b) == 0


def test_manual_channel_recorded(client) -> None:
    """channel=manual 也能绑定(手动填码兜底路径)。"""
    a = _login(client, "13800002009")
    b = _login(client, "13800002010")
    a_code = _my_code(client, a)
    r = client.post(
        "/api/v1/invite/bind",
        json={"invite_code": a_code, "channel": "manual"},
        headers=_auth(b),
    )
    assert r.status_code == 200, r.text
    assert r.json()["status"] == "success"


def test_invite_requires_auth(client) -> None:
    """不带 token → 401。"""
    assert client.get("/api/v1/invite/me").status_code == 401
    assert client.post("/api/v1/invite/bind", json={"invite_code": "ABCDEF"}).status_code == 401


def test_old_user_not_eligible(client) -> None:
    """新用户闸:被邀请人注册超过窗口 → not_eligible,双方都不发奖。"""
    a = _login(client, "13800002030")
    a_code = _my_code(client, a)
    b = _login(client, "13800002031")
    # 把 b 的注册时间改成超出窗口 → 变"老用户"
    with SessionLocal() as db:
        u = get_user_by_phone(db, "13800002031")
        assert u is not None
        u.created_at = datetime.now(timezone.utc) - timedelta(hours=INVITE_NEW_USER_WINDOW_HOURS + 1)
        db.commit()

    r = client.post("/api/v1/invite/bind", json={"invite_code": a_code}, headers=_auth(b))
    assert r.status_code == 200, r.text
    assert r.json()["status"] == "not_eligible"
    assert r.json()["coins_awarded"] == 0
    assert _coin_balance(client, b) == 0
    assert _coin_balance(client, a) == 0


# =====================================================================
# 任务 3:指纹归因(剪贴板兜底,见 brain [[invite-three-tasks]])
# =====================================================================

def test_landing_track_records_fingerprint(client) -> None:
    """落地页 POST /landing-track 成功存指纹(无需鉴权)。"""
    a = _login(client, "13800002040")
    a_code = _my_code(client, a)
    # 浏览器无 token 调
    r = client.post(
        "/api/v1/invite/landing-track",
        json={"ref": a_code, "screen": "1080x2400"},
    )
    assert r.status_code == 200, r.text
    assert r.json()["status"] == "ok"


def test_landing_track_invalid_code(client) -> None:
    """无效邀请码 → invalid_code(silent 返,不影响落地页下载流程)。"""
    r = client.post(
        "/api/v1/invite/landing-track",
        json={"ref": "ZZZZZZ", "screen": "1080x2400"},
    )
    assert r.status_code == 200, r.text
    assert r.json()["status"] == "invalid_code"


def test_bind_by_fingerprint_success(client) -> None:
    """B 落地页存指纹 → B 登录后用指纹兜底绑定成功(模拟剪贴板被覆盖丢失)。"""
    a = _login(client, "13800002041")
    a_code = _my_code(client, a)

    # 1. B 浏览器(无 token)访问落地页 → 存指纹
    r1 = client.post(
        "/api/v1/invite/landing-track",
        json={"ref": a_code, "screen": "1234x5678"},  # 用独特 screen 避免跟其它测试撞
    )
    assert r1.json()["status"] == "ok"

    # 2. B 注册登录(剪贴板被覆盖,没拿到邀请码)
    b = _login(client, "13800002042")

    # 3. B 不带 invite_code,只带 fingerprint 走兜底
    r2 = client.post(
        "/api/v1/invite/bind",
        json={
            "channel": "fingerprint",
            "fingerprint": {"screen": "1234x5678", "device_model": ""},
        },
        headers=_auth(b),
    )
    assert r2.status_code == 200, r2.text
    assert r2.json()["status"] == "success"
    # v3:绑定双方都不发钱(被邀请人无奖励、邀请人改比价发现金)
    assert _coin_balance(client, a) == 0
    assert _coin_balance(client, b) == 0


def test_bind_by_fingerprint_not_found(client) -> None:
    """没有匹配的指纹记录 → fp_not_found,不发奖。"""
    b = _login(client, "13800002043")
    r = client.post(
        "/api/v1/invite/bind",
        json={
            "channel": "fingerprint",
            "fingerprint": {"screen": "0000x9999", "device_model": "no_such_model"},
        },
        headers=_auth(b),
    )
    assert r.status_code == 200, r.text
    assert r.json()["status"] == "fp_not_found"
    assert r.json()["coins_awarded"] == 0
    assert _coin_balance(client, b) == 0


def test_bind_by_fingerprint_outside_window(client) -> None:
    """指纹记录超出 INVITE_FP_WINDOW_DAYS 窗口 → fp_not_found。

    用独特设备型号隔离:测试环境所有请求 IP 都是 testclient、落地页默认无 UA(解析出的
    型号="")，而指纹反查按 (IP, 型号) 匹配 → 会串到别的测试残留的、没过期的 model=""
    指纹上(误判 success)。这里给落地页传一个独特 UA(解析出独特型号 OUTWIN9X),反查只可能
    命中本测试自己的指纹,"过期就反查不到"才验得准。
    """
    a = _login(client, "13800002044")
    a_code = _my_code(client, a)

    # 落地页存指纹(独特 UA → 独特型号 OUTWIN9X),再手动把 created_at 改到窗口外
    unique_screen = "2222x3333"
    unique_ua = "Mozilla/5.0 (Linux; Android 13; OUTWIN9X Build/TQ) AppleWebKit/537.36"
    r1 = client.post(
        "/api/v1/invite/landing-track",
        json={"ref": a_code, "screen": unique_screen},
        headers={"user-agent": unique_ua},
    )
    assert r1.json()["status"] == "ok"

    with SessionLocal() as db:
        fp = db.execute(
            select(InviteFingerprint).where(InviteFingerprint.screen == unique_screen)
        ).scalar_one()
        fp.created_at = datetime.now(timezone.utc) - timedelta(days=INVITE_FP_WINDOW_DAYS + 1)
        db.commit()

    b = _login(client, "13800002045")
    r = client.post(
        "/api/v1/invite/bind",
        json={
            "channel": "fingerprint",
            "fingerprint": {"screen": unique_screen, "device_model": "OUTWIN9X"},
        },
        headers=_auth(b),
    )
    assert r.status_code == 200, r.text
    assert r.json()["status"] == "fp_not_found"
    assert _coin_balance(client, a) == 0
    assert _coin_balance(client, b) == 0


def test_bind_no_code_no_fingerprint(client) -> None:
    """既没邀请码也没指纹 → invalid_code(无效请求)。"""
    b = _login(client, "13800002046")
    r = client.post(
        "/api/v1/invite/bind",
        json={"channel": "fingerprint"},  # 没 invite_code、没 fingerprint
        headers=_auth(b),
    )
    assert r.status_code == 200, r.text
    assert r.json()["status"] == "invalid_code"
    assert _coin_balance(client, b) == 0


# =====================================================================
# 被邀请人列表 GET /invitees(邀请页小窗 + 完整列表页共用)
# =====================================================================

def test_invitees_basic(client) -> None:
    """A 邀 B、C → 列表返 2 条、total=2、名字=被邀请人默认昵称(创建即有)、头像 null、金币对。"""
    a = _login(client, "13800002050")
    a_code = _my_code(client, a)
    for phone in ("13800002051", "13800002052"):
        u = _login(client, phone)
        client.post("/api/v1/invite/bind", json={"invite_code": a_code}, headers=_auth(u))

    r = client.get("/api/v1/invite/invitees", headers=_auth(a))
    assert r.status_code == 200, r.text
    body = r.json()
    assert body["total"] == 2
    assert body["has_more"] is False
    assert len(body["items"]) == 2
    # 创建即分配默认昵称 → 名字=被邀请人昵称(取实际值对比)、头像 null(不依赖顺序用 set)
    with SessionLocal() as db:
        expected_names = {
            get_user_by_phone(db, "13800002051").nickname,
            get_user_by_phone(db, "13800002052").nickname,
        }
    names = {it["display_name"] for it in body["items"]}
    assert names == expected_names
    assert all(it["avatar_url"] is None for it in body["items"])
    # v2:绑定时邀请人那份为 0(收益改"好友比价才发奖"),故每条 coins 字段=0
    assert all(it["coins"] == 0 for it in body["items"])


def test_invitees_order_desc(client) -> None:
    """按邀请时间倒序:最近邀请的在最前(手动拉开时间,避开 SQLite 秒级精度)。"""
    from app.models.invite import InviteRelation
    a = _login(client, "13800002060")
    a_code = _my_code(client, a)
    b = _login(client, "13800002061")
    c = _login(client, "13800002062")
    client.post("/api/v1/invite/bind", json={"invite_code": a_code}, headers=_auth(b))
    client.post("/api/v1/invite/bind", json={"invite_code": a_code}, headers=_auth(c))
    with SessionLocal() as db:
        ub = get_user_by_phone(db, "13800002061")
        rel_b = db.execute(
            select(InviteRelation).where(InviteRelation.invitee_user_id == ub.id)
        ).scalar_one()
        rel_b.created_at = datetime.now(timezone.utc) - timedelta(hours=2)
        db.commit()
        nick_b = ub.nickname
        nick_c = get_user_by_phone(db, "13800002062").nickname
    items = client.get("/api/v1/invite/invitees", headers=_auth(a)).json()["items"]
    # 创建即有默认昵称 → display_name=被邀请人昵称;本用例核心验证倒序(C 刚邀在前、B 2h 前在后)
    assert items[0]["display_name"] == nick_c
    assert items[1]["display_name"] == nick_b


def test_invitees_nickname_priority(client) -> None:
    """设了昵称的被邀请人 → 名字用昵称(优先于脱敏手机号)。"""
    a = _login(client, "13800002070")
    a_code = _my_code(client, a)
    b = _login(client, "13800002071")
    client.post("/api/v1/invite/bind", json={"invite_code": a_code}, headers=_auth(b))
    with SessionLocal() as db:
        u = get_user_by_phone(db, "13800002071")
        u.nickname = "小明"
        db.commit()
    items = client.get("/api/v1/invite/invitees", headers=_auth(a)).json()["items"]
    assert items[0]["display_name"] == "小明"


def test_invitees_pagination(client) -> None:
    """分页:limit=1 → 1 条 + has_more=True;offset=1 → 第 2 条 + has_more=False。"""
    a = _login(client, "13800002080")
    a_code = _my_code(client, a)
    for phone in ("13800002081", "13800002082"):
        u = _login(client, phone)
        client.post("/api/v1/invite/bind", json={"invite_code": a_code}, headers=_auth(u))

    p1 = client.get("/api/v1/invite/invitees?limit=1&offset=0", headers=_auth(a)).json()
    assert len(p1["items"]) == 1 and p1["total"] == 2 and p1["has_more"] is True
    p2 = client.get("/api/v1/invite/invitees?limit=1&offset=1", headers=_auth(a)).json()
    assert len(p2["items"]) == 1 and p2["has_more"] is False


def test_invitees_empty_and_auth(client) -> None:
    """没邀请人 → 空列表;不带 token → 401。"""
    a = _login(client, "13800002090")
    body = client.get("/api/v1/invite/invitees", headers=_auth(a)).json()
    assert body["total"] == 0 and body["items"] == [] and body["has_more"] is False
    assert client.get("/api/v1/invite/invitees").status_code == 401
