"""后端 harvest 落库测试(比价记录改后端 harvest 后新增)。

覆盖:
  repo 层:harvest_running(建行/幂等回填)、harvest_done(派生+newly_success 幂等)、
          harvest_abort(夭折 / **不降级 success**)、upsert_record 不降级。
  端点层:price/step 首帧 mint trace_id + 回传 + 建 running 行;done 帧落 success;
          trace/finalize 落 cancelled;软鉴权(无 token 也放行、user_id 暂空)。
pricebot 用 httpx mock,不真连(同 test_compare_proxy)。
"""
from __future__ import annotations

import uuid
from unittest.mock import MagicMock, patch

import httpx
from sqlalchemy import select

from app.db.session import SessionLocal
from app.models.comparison import ComparisonRecord
from app.repositories import comparison as crud
from app.schemas.compare_record import ComparisonRecordIn


def _tid() -> str:
    return uuid.uuid4().hex


def _done_params() -> dict:
    """一份典型 done 帧 params:美团 25 元 vs 源淘宝闪购 30 元 → 省 5 元、success。"""
    return {
        "comparison_results": [
            {"platform_id": "meituan", "platform_name": "美团", "package": "com.sankuai.meituan",
             "price": 25.0, "is_source": False, "rank": 1, "items": []},
            {"platform_id": "taobao_flash", "platform_name": "淘宝闪购",
             "package": "com.taobao.taobao", "price": 30.0, "is_source": True, "rank": 2,
             "store_name": "测试店", "items": [{"name": "肥牛饭", "qty": 1}]},
        ],
        "information": "美团更便宜",
        "trace_url": "https://price.shaguabijia.com/traces/done/",
    }


def _get(db, trace_id: str) -> ComparisonRecord | None:
    return db.execute(
        select(ComparisonRecord).where(ComparisonRecord.trace_id == trace_id)
    ).scalar_one_or_none()


# ============================================================
# repo 层
# ============================================================


def test_harvest_running_creates_row(client) -> None:
    tid = _tid()
    with SessionLocal() as db:
        crud.harvest_running(
            db, trace_id=tid, user_id=None, device_id="dev-1",
            device_info={"brand": "vivo", "model": "V2309A", "android_version": "14",
                         "rom_version": "14"},
            trace_url="https://price.shaguabijia.com/traces/run/",
        )
        rec = _get(db, tid)
        assert rec is not None
        assert rec.status == "running"
        assert rec.user_id is None
        assert rec.device_id == "dev-1"
        assert rec.device_model == "V2309A"
        assert rec.device_manufacturer == "vivo"
        assert rec.rom_version == 14
        assert rec.trace_url.endswith("/run/")


def test_harvest_running_idempotent_backfills(client) -> None:
    tid = _tid()
    with SessionLocal() as db:
        crud.harvest_running(db, trace_id=tid, user_id=None, trace_url=None)
        # 第二帧带上 trace_url + user_id → 回填空缺,不新建、不改 status
        crud.harvest_running(db, trace_id=tid, user_id=None,
                             trace_url="https://price.shaguabijia.com/traces/late/")
        rows = db.execute(
            select(ComparisonRecord).where(ComparisonRecord.trace_id == tid)
        ).scalars().all()
        assert len(rows) == 1                       # 幂等:仍一行
        assert rows[0].status == "running"
        assert rows[0].trace_url.endswith("/late/")  # 空缺被回填


def test_harvest_done_derives_and_newly_success_once(client) -> None:
    tid = _tid()
    with SessionLocal() as db:
        crud.harvest_running(db, trace_id=tid, user_id=None)
        rec, newly = crud.harvest_done(db, trace_id=tid, user_id=None,
                                       done_params=_done_params())
        assert newly is True                        # running → success 是"新落成"
        assert rec.status == "success"
        assert rec.source_platform_id == "taobao_flash"
        assert rec.source_price_cents == 3000
        assert rec.best_platform_id == "meituan"
        assert rec.best_price_cents == 2500
        assert rec.saved_amount_cents == 500        # 30 - 25
        assert rec.is_source_best is False
        assert rec.store_name == "测试店"
        assert rec.information == "美团更便宜"
        assert rec.fail_reason is None               # 成功记录不派生失败原因
        assert rec.items == [{"name": "肥牛饭", "qty": 1}]
        assert rec.trace_url.endswith("/done/")
        # 再来一次(重试 done)→ 已 success,newly_success=False(发奖不重复触发)
        _rec2, newly2 = crud.harvest_done(db, trace_id=tid, user_id=None,
                                          done_params=_done_params())
        assert newly2 is False


def test_harvest_done_failed_derives_fail_reason(client) -> None:
    """failed 记录:记录级 information 笼统,但 fail_reason 从 platform_results 救出具体原因
    (id 3030 型:美团系统失败 + 京东 items_not_found → 展示京东那条)。"""
    tid = _tid()
    done_failed = {
        "comparison_results": [
            {"platform_id": "taobao_flash", "platform_name": "淘宝闪购",
             "package": "com.taobao.taobao", "price": 23.04, "is_source": True, "rank": 1,
             "items": [{"name": "肥牛饭", "qty": 1}]},
        ],
        "platform_results": {
            "taobao_flash": {"is_source": True, "status": "source", "price": 23.04},
            "meituan_waimai": {"is_source": False, "status": "failed",
                               "reason": "搜索店铺失败, 无法跳转到搜索页"},
            "jd_waimai_standalone": {"is_source": False, "status": "items_not_found",
                                     "reason": "京东外卖此店内未找到这些菜品"},
        },
        "information": "比价过程出错，请稍后重试",
    }
    with SessionLocal() as db:
        crud.harvest_running(db, trace_id=tid, user_id=None)
        rec, newly = crud.harvest_done(db, trace_id=tid, user_id=None,
                                       done_params=done_failed)
        assert newly is False                        # 没落成 success
        assert rec.status == "failed"
        assert rec.fail_reason == "京东外卖此店内未找到这些菜品"
        assert rec.information == "比价过程出错，请稍后重试"  # 原文案仍留存


def test_harvest_abort_cancels_running(client) -> None:
    tid = _tid()
    with SessionLocal() as db:
        crud.harvest_running(db, trace_id=tid, user_id=None)
        rec = crud.harvest_abort(db, trace_id=tid, status="cancelled",
                                 reason="用户终止比价",
                                 trace_url="https://price.shaguabijia.com/traces/ab/")
        assert rec is not None
        assert rec.status == "cancelled"
        assert rec.information == "用户终止比价"
        assert rec.trace_url.endswith("/ab/")


def test_harvest_abort_no_downgrade_success(client) -> None:
    """已 success 的行,finalize 后到(收尾取消)→ 只 refresh trace_url,status 不降级。"""
    tid = _tid()
    with SessionLocal() as db:
        crud.harvest_running(db, trace_id=tid, user_id=None)
        crud.harvest_done(db, trace_id=tid, user_id=None, done_params=_done_params())
        rec = crud.harvest_abort(db, trace_id=tid, status="cancelled", reason="收尾误触")
        assert rec.status == "success"              # 不降级
        assert rec.information == "美团更便宜"       # 不被 abort 的 reason 覆盖


def test_harvest_abort_missing_row_returns_none(client) -> None:
    with SessionLocal() as db:
        assert crud.harvest_abort(db, trace_id=_tid(), status="cancelled",
                                  reason=None) is None


def test_upsert_record_no_downgrade_after_harvest_success(client) -> None:
    """harvest 落 success 后,老客户端 fromFailure 的 cancelled 上报不许把它盖回去。"""
    tid = _tid()
    with SessionLocal() as db:
        crud.harvest_done(db, trace_id=tid, user_id=None, done_params=_done_params())
        payload = ComparisonRecordIn(
            trace_id=tid, business_type="food", status="cancelled",
            information="用户终止", comparison_results=[],
        )
        # 用一个不会与顺序自增用户撞的合成 id(SQLite 测试库 FK 不强制;别用小整数,
        # 否则会撞上别的测试 login 出来的真实 user_id → 记录混进那个用户的列表)。
        rec = crud.upsert_record(db, user_id=987654, payload=payload)
        assert rec.status == "success"              # 不降级
        assert rec.user_id == 987654                 # 但补上了 user_id(原为 None)


# ============================================================
# 端点层(mock pricebot)
# ============================================================


def _mock_pricebot(resp_json: dict):
    """patch httpx.AsyncClient.post 返回给定响应;捕获转发的 content。"""
    captured: dict = {}

    async def fake_post(self, url, content=None, **kw):
        captured["url"] = url
        captured["content"] = content
        m = MagicMock()
        m.status_code = 200
        m.json = lambda: dict(resp_json)
        return m

    return patch.object(httpx.AsyncClient, "post", fake_post), captured


def _stub_body(**over) -> dict:
    body = {
        "device_id": "dev-x",
        "step": 0,
        "query": "海底捞",
        "device_info": {"brand": "OPPO", "model": "PEXM00", "android_version": "13",
                        "rom_version": "13"},
        "screen_state": {"screen": {"width": 1080, "height": 2340, "density": 3.0},
                         "foreground": {"package": "com.taobao.taobao", "activity": ""},
                         "windows": []},
    }
    body.update(over)
    return body


def test_price_step_mints_trace_id_and_creates_running(client) -> None:
    """首帧不带 trace_id → app-server 签发 + 回传;并建 running 行(带机型/设备)。"""
    wait_frame = {"success": True, "action": {"command": "wait", "params": {"duration_ms": 500}},
                  "continue": True, "trace_url": "https://price.shaguabijia.com/traces/mint/"}
    p, _cap = _mock_pricebot(wait_frame)
    with p:
        r = client.post("/api/v1/price/step", json=_stub_body())  # 无 trace_id、无 token
    assert r.status_code == 200, r.text
    tid = r.json().get("trace_id")
    assert tid and len(tid) >= 16                     # 回传了签发的 trace_id
    with SessionLocal() as db:
        rec = _get(db, tid)
        assert rec is not None and rec.status == "running"
        assert rec.user_id is None                    # 无 token → 软鉴权放行、user_id 暂空
        assert rec.device_model == "PEXM00"
        assert rec.trace_url.endswith("/mint/")


def test_price_step_done_harvests_success(client) -> None:
    tid = _tid()
    done_frame = {"success": True, "continue": False,
                  "action": {"command": "done", "params": _done_params()},
                  "trace_url": "https://price.shaguabijia.com/traces/done2/"}
    p, _cap = _mock_pricebot(done_frame)
    with p, patch(
        "app.api.v1.compare.backfill_comparison_llm_cost"
    ) as backfill:
        r = client.post("/api/v1/price/step", json=_stub_body(trace_id=tid, step=8))
    assert r.status_code == 200
    with SessionLocal() as db:
        rec = _get(db, tid)
        assert rec is not None and rec.status == "success"
        assert rec.best_platform_id == "meituan"
        assert rec.saved_amount_cents == 500
        backfill.assert_called_once_with(rec.id, tid)


def test_trace_finalize_harvests_abort(client) -> None:
    tid = _tid()
    with SessionLocal() as db:                        # 先有 running 行(帧0建的)
        crud.harvest_running(db, trace_id=tid, user_id=None)
    p, _cap = _mock_pricebot({"trace_url": "https://price.shaguabijia.com/traces/fin/"})
    with p, patch(
        "app.api.v1.compare.backfill_comparison_llm_cost"
    ) as backfill:
        r = client.post("/api/v1/trace/finalize",
                        json={"trace_id": tid, "status": "cancelled", "reason": "用户终止"})
    assert r.status_code == 200
    with SessionLocal() as db:
        rec = _get(db, tid)
        assert rec is not None and rec.status == "cancelled"
        assert rec.trace_url.endswith("/fin/")
        backfill.assert_called_once_with(rec.id, tid)


def test_price_step_binds_user_when_authed(client) -> None:
    """带 JWT 的首帧 → running 行绑上 user_id。"""
    client.post("/api/v1/auth/sms/send", json={"phone": "13800009001"})
    token = client.post("/api/v1/auth/sms/login",
                        json={"phone": "13800009001", "code": "123456"}).json()["access_token"]
    wait_frame = {"success": True, "action": {"command": "wait", "params": {}},
                  "continue": True, "trace_url": "https://price.shaguabijia.com/traces/auth/"}
    p, _cap = _mock_pricebot(wait_frame)
    with p:
        r = client.post("/api/v1/price/step", json=_stub_body(),
                        headers={"Authorization": f"Bearer {token}"})
    tid = r.json()["trace_id"]
    with SessionLocal() as db:
        rec = _get(db, tid)
        assert rec is not None and rec.user_id is not None   # 绑上了登录用户
