"""比价记录测试:上报派生 / 幂等 / 分页 / 详情 / 鉴权 / 失败帧。

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


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 _food_payload(trace_id: str) -> dict:
    """一次淘宝闪购→美团/京东 的成功比价(美团最便宜)。"""
    return {
        "trace_id": trace_id,
        "business_type": "food",
        "device_id": "dev-abc",
        "store_name": "海底捞(朝阳店)",
        "source_platform_id": "taobao_flash",
        "source_platform_name": "淘宝闪购",
        "source_package": "com.taobao.taobao",
        "source_price": 128.50,
        "items": [
            {"name": "麻辣午餐肉", "qty": 2},
            {"name": "黑牛肉卷", "qty": 1, "specs": ["大份"]},
        ],
        "comparison_results": [
            {"platform_id": "meituan", "platform_name": "美团", "package": "com.sankuai.meituan",
             "price": 123.50, "is_source": False, "rank": 1,
             "coupon_saved": 7.0, "coupon_name": "外卖大额神券"},
            {"platform_id": "taobao_flash", "platform_name": "淘宝闪购", "package": "com.taobao.taobao",
             "price": 128.50, "is_source": True, "rank": 2},
            {"platform_id": "jd_waimai", "platform_name": "京东外卖", "package": "com.jingdong.app.mall",
             "price": 130.00, "is_source": False, "rank": 3},
        ],
        "skipped_dish_count": 1,
        "skipped_dish_names": ["黑牛肉卷"],
        "total_dish_count": 3,
        "information": "在美团找到同店,到手价 ¥123.50",
        "total_ms": 12345,
    }


def test_report_and_derive(client) -> None:
    """上报成功比价:服务端派生 best/saved/is_source_best/分,详情可读回。"""
    token = _login(client, "13800002001")

    r = client.post("/api/v1/compare/record", json=_food_payload("trace-1"), headers=_auth(token))
    assert r.status_code == 200, r.text
    rec_id = r.json()["id"]
    assert isinstance(rec_id, int)

    r = client.get(f"/api/v1/compare/records/{rec_id}", headers=_auth(token))
    assert r.status_code == 200, r.text
    d = r.json()
    # 金额转分
    assert d["source_price_cents"] == 12850
    assert d["best_price_cents"] == 12350
    # 省 = 源价 - 最优价 = 5.00 元
    assert d["saved_amount_cents"] == 500
    # 最优是美团(rank=1),非源平台 → 没"源平台最便宜"
    assert d["best_platform_id"] == "meituan"
    assert d["is_source_best"] is False
    assert d["status"] == "success"
    assert d["information"] == "在美团找到同店,到手价 ¥123.50"
    assert d["store_name"] == "海底捞(朝阳店)"
    assert d["total_dish_count"] == 3
    assert d["total_ms"] == 12345
    assert d["raw_payload"]["total_ms"] == 12345
    assert d["skipped_dish_count"] == 1
    assert d["skipped_dish_names"] == ["黑牛肉卷"]
    assert len(d["comparison_results"]) == 3
    assert len(d["items"]) == 2
    # 详情带 raw_payload 全量
    assert d["raw_payload"]["trace_id"] == "trace-1"


def test_coupon_saved_passthrough(client) -> None:
    """红包优惠额 coupon_saved + 来源名 coupon_name 上报→落库→读出;仅目标平台带值,源平台不带(null)。"""
    token = _login(client, "13800002010")
    rid = client.post(
        "/api/v1/compare/record", json=_food_payload("trace-coupon"), headers=_auth(token)
    ).json()["id"]
    d = client.get(f"/api/v1/compare/records/{rid}", headers=_auth(token)).json()
    by_pid = {x["platform_id"]: x for x in d["comparison_results"]}
    assert by_pid["meituan"]["coupon_saved"] == 7.0            # 目标平台带值
    assert by_pid["meituan"]["coupon_name"] == "外卖大额神券"   # 来源名透传(pricebot#38)
    assert by_pid["taobao_flash"].get("coupon_saved") is None  # 源平台不带/为 null
    assert by_pid["taobao_flash"].get("coupon_name") is None   # 源平台无来源名


def test_source_is_cheapest_no_saving(client) -> None:
    """源平台本来就最便宜:saved<=0,is_source_best=True,仍记一条 success。"""
    token = _login(client, "13800002002")
    payload = _food_payload("trace-2")
    # 把源平台价改到最低 + rank=1
    payload["source_price"] = 100.00
    payload["comparison_results"] = [
        {"platform_id": "taobao_flash", "platform_name": "淘宝闪购", "package": "com.taobao.taobao",
         "price": 100.00, "is_source": True, "rank": 1},
        {"platform_id": "meituan", "platform_name": "美团", "package": "com.sankuai.meituan",
         "price": 123.50, "is_source": False, "rank": 2},
    ]
    r = client.post("/api/v1/compare/record", json=payload, headers=_auth(token))
    rec_id = r.json()["id"]
    d = client.get(f"/api/v1/compare/records/{rec_id}", headers=_auth(token)).json()
    assert d["best_platform_id"] == "taobao_flash"
    assert d["is_source_best"] is True
    assert d["saved_amount_cents"] == 0  # 100 - 100
    assert d["status"] == "success"  # 有非源有效价(美团)


def test_failed_comparison_recorded(client) -> None:
    """比价失败(只有源、无目标有效价):status=failed 也落一条。"""
    token = _login(client, "13800002003")
    payload = _food_payload("trace-3")
    payload["comparison_results"] = [
        {"platform_id": "taobao_flash", "platform_name": "淘宝闪购", "package": "com.taobao.taobao",
         "price": 128.50, "is_source": True, "rank": 1},
    ]
    r = client.post("/api/v1/compare/record", json=payload, headers=_auth(token))
    rec_id = r.json()["id"]
    d = client.get(f"/api/v1/compare/records/{rec_id}", headers=_auth(token)).json()
    assert d["status"] == "failed"
    assert d["best_platform_id"] == "taobao_flash"  # 唯一一条
    # 源价有、最优价=源价 → saved=0(没采到更便宜目标)
    assert d["saved_amount_cents"] == 0


def test_idempotent_same_trace(client) -> None:
    """同 trace_id 重复上报:返回同一 id,只一条记录,后报覆盖。"""
    token = _login(client, "13800002004")
    p = _food_payload("trace-dup")
    r1 = client.post("/api/v1/compare/record", json=p, headers=_auth(token))
    id1 = r1.json()["id"]

    # 改个值再报一次(同 trace)
    p["store_name"] = "改名后的店"
    r2 = client.post("/api/v1/compare/record", json=p, headers=_auth(token))
    id2 = r2.json()["id"]
    assert id1 == id2  # 同一条

    # 列表里只有一条
    page = client.get("/api/v1/compare/records", headers=_auth(token)).json()
    assert len(page["items"]) == 1
    # 覆盖生效
    assert page["items"][0]["store_name"] == "改名后的店"


def test_records_pagination_and_isolation(client) -> None:
    """分页 id 倒序 + 游标;只看得到自己的记录。"""
    token_a = _login(client, "13800002005")
    for i in range(5):
        client.post("/api/v1/compare/record", json=_food_payload(f"a-{i}"), headers=_auth(token_a))

    # 另一个用户的记录不应混入
    token_b = _login(client, "13800002006")
    client.post("/api/v1/compare/record", json=_food_payload("b-0"), headers=_auth(token_b))

    r = client.get("/api/v1/compare/records?limit=3", headers=_auth(token_a))
    page1 = r.json()
    assert len(page1["items"]) == 3
    assert page1["next_cursor"] is not None
    ids = [it["id"] for it in page1["items"]]
    assert ids == sorted(ids, reverse=True)

    r = client.get(
        f"/api/v1/compare/records?limit=3&cursor={page1['next_cursor']}",
        headers=_auth(token_a),
    )
    page2 = r.json()
    assert len(page2["items"]) == 2  # A 共 5 条
    assert max(it["id"] for it in page2["items"]) < min(ids)

    # B 只看到自己那一条
    pb = client.get("/api/v1/compare/records", headers=_auth(token_b)).json()
    assert len(pb["items"]) == 1


def test_detail_cross_user_404(client) -> None:
    """读他人记录详情 → 404(限本人)。"""
    token_a = _login(client, "13800002007")
    rid = client.post(
        "/api/v1/compare/record", json=_food_payload("x-0"), headers=_auth(token_a)
    ).json()["id"]

    token_b = _login(client, "13800002008")
    r = client.get(f"/api/v1/compare/records/{rid}", headers=_auth(token_b))
    assert r.status_code == 404


def test_stats_compare_count_and_saved(client) -> None:
    """比价口径战绩:完成比价数 + 累计发现可省(各成功比价 saved 之和)。"""
    token = _login(client, "13800002011")
    # 新用户:无记录 → 全 0(配合「我的」页未比价锁定态)
    s0 = client.get("/api/v1/compare/stats", headers=_auth(token)).json()
    assert s0["compare_count"] == 0
    assert s0["discovered_saved_cents"] == 0

    # 报 2 条成功比价(各省 128.50−123.50=5.00 元=500 分)
    client.post("/api/v1/compare/record", json=_food_payload("stat-1"), headers=_auth(token))
    client.post("/api/v1/compare/record", json=_food_payload("stat-2"), headers=_auth(token))
    s = client.get("/api/v1/compare/stats", headers=_auth(token)).json()
    assert s["compare_count"] == 2
    assert s["discovered_saved_cents"] == 1000

    # 失败帧(只有源平台)不计入成功比价数
    fail = _food_payload("stat-fail")
    fail["comparison_results"] = [
        {"platform_id": "taobao_flash", "platform_name": "淘宝闪购",
         "package": "com.taobao.taobao", "price": 128.50, "is_source": True, "rank": 1},
    ]
    client.post("/api/v1/compare/record", json=fail, headers=_auth(token))
    s2 = client.get("/api/v1/compare/stats", headers=_auth(token)).json()
    assert s2["compare_count"] == 2  # 仍 2(failed 不计)


def test_records_ordered_flag(client) -> None:
    """「已下单」店级标记:店名命中该用户 source='compare' 的下单记录才 True。

    覆盖 list_records 只按**本页店名**反查 savings 的写法(原来是把该用户全部下单店名捞回内存
    再取交集,随下单量线性变慢)——两种写法结果必须一致,故这里按店名逐条断言。
    """
    token = _login(client, "13800002010")

    # 两条比价记录:一条海底捞(稍后会有对应下单),一条没下过单的店
    client.post("/api/v1/compare/record", json=_food_payload("ord-1"), headers=_auth(token))
    other = _food_payload("ord-2")
    other["store_name"] = "没下过单的店"
    client.post("/api/v1/compare/record", json=other, headers=_auth(token))

    # 下单前:两条都不该带「已下单」
    items = client.get("/api/v1/compare/records", headers=_auth(token)).json()["items"]
    assert {it["store_name"]: it["ordered"] for it in items} == {
        "海底捞(朝阳店)": False,
        "没下过单的店": False,
    }

    # 对海底捞真实下单一笔(order/report 写 source='compare' 的 savings_record)
    r = client.post(
        "/api/v1/order/report",
        json={
            "client_event_id": "evt-ordered-flag",
            "platform": "美团",
            "platform_package": "com.sankuai.meituan",
            "pay_channel": "wechat",
            "compared_price_cents": 12350,
            "paid_amount_cents": 12350,
            "shop_name": "海底捞(朝阳店)",
            "original_price_cents": 12850,
        },
        headers=_auth(token),
    )
    assert r.status_code == 200, r.text

    # 下单后:只有同店名那条翻成 True,另一条不受影响
    items = client.get("/api/v1/compare/records", headers=_auth(token)).json()["items"]
    assert {it["store_name"]: it["ordered"] for it in items} == {
        "海底捞(朝阳店)": True,
        "没下过单的店": False,
    }

    # 别人的下单不该影响本人标记(_ordered_shop_names 按 user_id 过滤)
    token_b = _login(client, "13800002011")
    client.post("/api/v1/compare/record", json=_food_payload("ord-b"), headers=_auth(token_b))
    items_b = client.get("/api/v1/compare/records", headers=_auth(token_b)).json()["items"]
    assert [it["ordered"] for it in items_b] == [False]


def test_records_list_omits_raw_payload(client) -> None:
    """列表出参不含 raw_payload(仓库层 defer 掉了重型 JSON 列);详情接口照常返回。

    defer 的列一旦被 ORM 实例读到会触发逐行懒加载(N+1),而列表 schema 本就不该带 raw_payload
    —— 这条同时守住「列表不泄露上报体全量」和「没人不小心把它加回出参」。
    """
    token = _login(client, "13800002012")
    rid = client.post(
        "/api/v1/compare/record", json=_food_payload("no-raw"), headers=_auth(token)
    ).json()["id"]

    items = client.get("/api/v1/compare/records", headers=_auth(token)).json()["items"]
    assert len(items) == 1
    assert "raw_payload" not in items[0]
    # 概要字段照常齐全(defer 没误伤列表要用的列)
    assert items[0]["store_name"] == "海底捞(朝阳店)"
    assert items[0]["best_platform_id"] == "meituan"
    assert items[0]["comparison_results"] and items[0]["items"]

    # 详情不 defer:raw_payload 全量还在
    d = client.get(f"/api/v1/compare/records/{rid}", headers=_auth(token)).json()
    assert d["raw_payload"]["trace_id"] == "no-raw"


def test_records_ordered_filter(client) -> None:
    """ordered=true 只出「已下单」的记录,且过滤结果自身能翻页。

    这个筛选必须在服务端做:客户端早先是对「已经拉回来的那一页」做 filter,分页之后一页里
    很可能一条已下单都没有 ——「已下单」tab 就会看着像空的,得手动翻很多页才蹦出一条。
    """
    token = _login(client, "13800002013")

    # 3 条「下过单的店」+ 2 条没下过单的店,交错写入,确保过滤不是靠顺序碰巧对上
    for i in range(3):
        p = _food_payload(f"of-ordered-{i}")
        p["store_name"] = "下过单的店"
        client.post("/api/v1/compare/record", json=p, headers=_auth(token))
        if i < 2:
            q = _food_payload(f"of-plain-{i}")
            q["store_name"] = "没下过单的店"
            client.post("/api/v1/compare/record", json=q, headers=_auth(token))

    client.post(
        "/api/v1/order/report",
        json={
            "client_event_id": "evt-ordered-filter",
            "platform": "美团",
            "platform_package": "com.sankuai.meituan",
            "pay_channel": "wechat",
            "compared_price_cents": 12350,
            "paid_amount_cents": 12350,
            "shop_name": "下过单的店",
            "original_price_cents": 12850,
        },
        headers=_auth(token),
    )

    # 不传 ordered:5 条全出(「全部记录」tab 口径不变)
    assert len(client.get("/api/v1/compare/records", headers=_auth(token)).json()["items"]) == 5

    # ordered=true:只出那 3 条,且每条都自带 ordered=True
    page = client.get("/api/v1/compare/records?ordered=true", headers=_auth(token)).json()
    assert [it["store_name"] for it in page["items"]] == ["下过单的店"] * 3
    assert all(it["ordered"] for it in page["items"])
    assert page["next_cursor"] is None

    # 游标只在「已下单」集合内走 —— 不会把没下单的记录算进一页的 limit 里
    p1 = client.get(
        "/api/v1/compare/records?ordered=true&limit=2", headers=_auth(token)
    ).json()
    assert len(p1["items"]) == 2
    assert p1["next_cursor"] is not None
    p2 = client.get(
        f"/api/v1/compare/records?ordered=true&limit=2&cursor={p1['next_cursor']}",
        headers=_auth(token),
    ).json()
    assert [it["store_name"] for it in p2["items"]] == ["下过单的店"]
    # 两页不重叠,合起来正好 3 条
    assert len({it["id"] for it in p1["items"] + p2["items"]}) == 3

    # 别人的下单不该让本人记录进「已下单」
    token_b = _login(client, "13800002014")
    pb = _food_payload("of-b")
    pb["store_name"] = "下过单的店"
    client.post("/api/v1/compare/record", json=pb, headers=_auth(token_b))
    assert client.get(
        "/api/v1/compare/records?ordered=true", headers=_auth(token_b)
    ).json()["items"] == []


def test_records_keyword_search(client) -> None:
    """keyword 按店名 / 菜名模糊搜(忽略大小写),LIKE 通配符只当字面量;搜索结果也能翻页。

    菜名走写路径派生的 product_names 文本列 —— items 是 JSON,SQLite 下中文被 ensure_ascii
    转义,直接 LIKE 搜不到。
    """
    token = _login(client, "13800002015")

    b = _food_payload("kw-b")
    b["store_name"] = "Pizza Hut"
    b["items"] = [{"name": "榴莲比萨", "qty": 1}]
    client.post("/api/v1/compare/record", json=b, headers=_auth(token))

    c = _food_payload("kw-c")
    c["store_name"] = "100%纯牛肉汉堡"
    c["items"] = [{"name": "双层牛肉堡", "qty": 1}]
    client.post("/api/v1/compare/record", json=c, headers=_auth(token))

    # 默认 payload 的店名是「海底捞(朝阳店)」
    client.post("/api/v1/compare/record", json=_food_payload("kw-a"), headers=_auth(token))

    def _search(kw: str, **extra) -> list[str]:
        r = client.get(
            "/api/v1/compare/records",
            params={"keyword": kw, **extra},
            headers=_auth(token),
        )
        assert r.status_code == 200, r.text
        return [it["store_name"] for it in r.json()["items"]]

    assert _search("海底捞") == ["海底捞(朝阳店)"]          # 店名命中
    assert _search("榴莲") == ["Pizza Hut"]                  # 菜名命中(product_names)
    assert _search("pizza") == ["Pizza Hut"]                 # 忽略大小写
    assert _search("PIZZA") == ["Pizza Hut"]
    assert _search("不存在的店") == []                        # 没命中就是空
    # 通配符只当普通字符:搜 % 不该把整表拉回来,搜 _ 也不该匹配任意单字符
    assert _search("%") == ["100%纯牛肉汉堡"]
    assert _search("_") == []
    # 纯空白等同不传 → 不过滤
    assert len(_search("   ")) == 3

    # 搜索结果自身可翻页
    for i in range(3):
        p = _food_payload(f"kw-page-{i}")
        p["store_name"] = "连锁烤鱼店"
        client.post("/api/v1/compare/record", json=p, headers=_auth(token))
    p1 = client.get(
        "/api/v1/compare/records",
        params={"keyword": "烤鱼", "limit": 2},
        headers=_auth(token),
    ).json()
    assert len(p1["items"]) == 2
    assert p1["next_cursor"] is not None
    p2 = client.get(
        "/api/v1/compare/records",
        params={"keyword": "烤鱼", "limit": 2, "cursor": p1["next_cursor"]},
        headers=_auth(token),
    ).json()
    assert [it["store_name"] for it in p2["items"]] == ["连锁烤鱼店"]
    assert len({it["id"] for it in p1["items"] + p2["items"]}) == 3


def test_requires_auth(client) -> None:
    """不带 token 统一 401。"""
    assert client.post("/api/v1/compare/record", json={"trace_id": "t"}).status_code == 401
    assert client.get("/api/v1/compare/records").status_code == 401
    assert client.get("/api/v1/compare/records/1").status_code == 401
    assert client.get("/api/v1/compare/stats").status_code == 401


def test_trace_id_required(client) -> None:
    """缺 trace_id → 422(pydantic 校验)。"""
    token = _login(client, "13800002009")
    r = client.post("/api/v1/compare/record", json={"business_type": "food"}, headers=_auth(token))
    assert r.status_code == 422
