"""新接口的端到端测试。会真实跑到 mock_extractor,因此每个 case 会 sleep 1.5-2.5s。
我们 monkey-patch 把 sleep 拿掉,加快测试。"""
from __future__ import annotations

import io
import json
from unittest.mock import patch

import pytest
from fastapi.testclient import TestClient

from app.main import app

client = TestClient(app)


@pytest.fixture(autouse=True)
def _no_sleep():
    """禁用 mock_extractor 的真实异步延迟,测试运行时间从 ~10s 降到 < 1s。"""
    async def _instant(_seconds):
        return None
    with patch("app.mock_extractor.asyncio.sleep", _instant):
        yield


def _fake_image(payload: bytes = b"fake jpeg bytes 0123456789") -> tuple:
    return ("image", ("test.jpg", io.BytesIO(payload), "image/jpeg"))


def test_parse_image_basic() -> None:
    r = client.post(
        "/api/v1/parse-image",
        files=[_fake_image()],
        data={"clusters": "[]"},
    )
    assert r.status_code == 200, r.text
    body = r.json()
    assert body["title"]
    assert body["price"] > 0
    assert body["typical_price"] > 0
    assert body["source_app"] in {"淘宝", "京东", "拼多多", "抖音", "美团", "饿了么"}
    assert body["cluster_id"] is None  # 空 clusters 必然新建


def test_parse_image_same_bytes_same_product() -> None:
    """同样字节 → 同样商品(便于真机重放)。允许价格因抖动小幅不同。"""
    r1 = client.post("/api/v1/parse-image", files=[_fake_image(b"AAAA")], data={"clusters": "[]"})
    r2 = client.post("/api/v1/parse-image", files=[_fake_image(b"AAAA")], data={"clusters": "[]"})
    assert r1.status_code == 200
    assert r2.status_code == 200
    assert r1.json()["title"] == r2.json()["title"]
    assert r1.json()["source_app"] == r2.json()["source_app"]


def test_parse_image_cluster_match() -> None:
    """如果 clusters 里有 title 与 mock 选中商品共享前 4 字符,应命中该 cluster。"""
    # 先发一次拿到 picked title
    r1 = client.post("/api/v1/parse-image", files=[_fake_image(b"BBBB")], data={"clusters": "[]"})
    picked_title = r1.json()["title"]
    cluster_id = "uuid-fake-1"
    clusters = [{"id": cluster_id, "title": picked_title}]
    r2 = client.post(
        "/api/v1/parse-image",
        files=[_fake_image(b"BBBB")],
        data={"clusters": json.dumps(clusters)},
    )
    assert r2.status_code == 200
    assert r2.json()["cluster_id"] == cluster_id


def test_parse_image_no_image() -> None:
    r = client.post(
        "/api/v1/parse-image",
        files=[("image", ("empty.jpg", io.BytesIO(b""), "image/jpeg"))],
        data={"clusters": "[]"},
    )
    assert r.status_code == 422
    assert r.json()["detail"] == "no_image"


def test_parse_image_missing_image_field() -> None:
    r = client.post("/api/v1/parse-image", data={"clusters": "[]"})
    assert r.status_code == 422  # FastAPI 字段校验失败也返回 422


def test_parse_image_invalid_clusters_json() -> None:
    r = client.post(
        "/api/v1/parse-image",
        files=[_fake_image()],
        data={"clusters": "not a json"},
    )
    assert r.status_code == 422
    assert r.json()["detail"] == "invalid_clusters"


def test_parse_image_clusters_not_array() -> None:
    r = client.post(
        "/api/v1/parse-image",
        files=[_fake_image()],
        data={"clusters": '{"id":"x","title":"y"}'},
    )
    assert r.status_code == 422
    assert r.json()["detail"] == "invalid_clusters"


def test_parse_image_too_large() -> None:
    big = b"x" * (5 * 1024 * 1024 + 1)
    r = client.post(
        "/api/v1/parse-image",
        files=[("image", ("big.jpg", io.BytesIO(big), "image/jpeg"))],
        data={"clusters": "[]"},
    )
    assert r.status_code == 422
    assert r.json()["detail"] == "image_too_large"


def test_parse_text_basic() -> None:
    r = client.post(
        "/api/v1/parse-text",
        json={
            "title": "iPhone 15 Pro Max 256GB",
            "price": 9999.0,
            "source_app": "淘宝",
            "clusters": [],
        },
    )
    assert r.status_code == 200
    body = r.json()
    assert body["title"] == "iPhone 15 Pro Max 256GB"
    assert body["price"] == 9999.0
    assert body["source_app"] == "淘宝"
    assert body["typical_price"] > 0
    assert body["cluster_id"] is None


def test_parse_text_cluster_match() -> None:
    """同样 title 前 4 字符的 cluster 应命中。"""
    cluster_id = "uuid-text-1"
    r = client.post(
        "/api/v1/parse-text",
        json={
            "title": "iPhone 15 Pro Max 1TB 暮光紫",
            "price": 11999.0,
            "source_app": "京东",
            "clusters": [{"id": cluster_id, "title": "iPhone 15 Pro 256GB"}],
        },
    )
    assert r.status_code == 200
    assert r.json()["cluster_id"] == cluster_id


def test_parse_text_empty_title() -> None:
    r = client.post(
        "/api/v1/parse-text",
        json={"title": "  ", "price": 9.9, "source_app": "淘宝", "clusters": []},
    )
    assert r.status_code == 422
    assert r.json()["detail"] == "empty_title"


def test_parse_text_invalid_price() -> None:
    r = client.post(
        "/api/v1/parse-text",
        json={"title": "x", "price": 0, "source_app": "淘宝", "clusters": []},
    )
    assert r.status_code == 422
    assert r.json()["detail"] == "invalid_price"


def test_parse_text_negative_price() -> None:
    r = client.post(
        "/api/v1/parse-text",
        json={"title": "x", "price": -1, "source_app": "淘宝", "clusters": []},
    )
    assert r.status_code == 422


def test_parse_image_invalid_cluster_schema() -> None:
    """clusters 数组里某个对象缺 id/title 字段 → Pydantic ValidationError 必须返回 422 而非 500"""
    r = client.post(
        "/api/v1/parse-image",
        files=[_fake_image()],
        data={"clusters": '[{"id":"only-id-no-title"}]'},
    )
    assert r.status_code == 422
    assert r.json()["detail"] == "invalid_clusters"


def test_concurrent_parse_image_does_not_block_event_loop(_no_sleep) -> None:
    """禁用 sleep 后并发应该极快;若 event loop 被阻塞会显著退化。
    主要保护 mock_extract_image 永远是 async + await。"""
    import time as _t
    start = _t.time()
    for _ in range(10):
        r = client.post("/api/v1/parse-image", files=[_fake_image()], data={"clusters": "[]"})
        assert r.status_code == 200
    # 10 次串行(TestClient 是同步客户端)且 sleep 被 mock 掉,应远 < 1s
    assert _t.time() - start < 2.0
