"""
AI 引擎单元测试（核心逻辑覆盖）
对照 JS 版逻辑验证 Python 重写正确性
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

import pytest
from app.services.ai_engine.text_scoring import score_copy, is_similar_copy, dedupe_copies
from app.services.ai_engine.banned_word_checker import check_and_fix, BannedWordEntry
from app.services.ai_engine.storyboard import get_narrative_roles, plan_image_set, clamp_count
from app.services.ai_engine.preference_aggregator import aggregate_preference_context, collect_preference_event
from app.services.ai_engine.prompt_composer import (
    compose_variants, compose_preference_context, compose_image_prompt, parse_model_output
)

# ── 测试用 product ─────────────────────────────────────────
PRODUCT = {
    "name": "倍分子素颜霜",
    "category": "美妆护肤",
    "selling_points": ["轻薄不厚重", "水润自然", "不卡粉"],
    "keywords": ["素颜霜", "日常通勤"],
    "style_tone": "素人分享风",
    "text_angles": ["痛点切入", "场景型", "避坑型"],
    "custom_prompt": "",
    "target_audience": "上班族",
}

GOOD_COPY = {
    "title": "早八素颜也能有点气色！✨",
    "content": "姐妹们，最近实测下来，轻薄不厚重是我最在意的。✅ 通勤路上随手一抹，水润自然有气色。不卡粉这点太加分了。整体就是那种上班族能马上代入的日常好物。",
    "tags": ["#素颜霜", "#日常通勤", "#好物分享", "#真实测评"],
    "angle": "场景型",
    "buyingPoint": "轻薄不厚重，适合上班族",
    "coverTitle": "早八素颜也能有点气色",
    "imageBrief": "封面自然光上脸局部，内页质地推开+软性转化。",
    "source": "ai",
}


# ── 五维打分 ───────────────────────────────────────────────
class TestScoreCopy:
    def test_good_copy_passes(self):
        result = score_copy(GOOD_COPY, PRODUCT)
        assert result["passed"] is True
        assert result["score"] >= 90

    def test_banned_word_kills_compliance(self):
        bad = {**GOOD_COPY, "title": "美白素颜霜强推！", "content": GOOD_COPY["content"] + "美白效果显著"}
        result = score_copy(bad, PRODUCT)
        # 含美白 → 合规0分 + 总分不过
        assert result["passed"] is False
        compliance = next(d for d in result["score_detail"] if d["item"] == "合规性")
        assert compliance["score"] == 0

    def test_score_detail_has_five_dims(self):
        result = score_copy(GOOD_COPY, PRODUCT)
        assert len(result["score_detail"]) == 5
        items = {d["item"] for d in result["score_detail"]}
        assert "标题吸引力" in items
        assert "情绪共鸣" in items
        assert "买点表达" in items
        assert "关键词覆盖" in items
        assert "合规性" in items


# ── 去重 ───────────────────────────────────────────────────
class TestDedup:
    def test_identical_title_deduped(self):
        a = {**GOOD_COPY}
        b = {**GOOD_COPY, "content": "略有不同的内容，但标题一样"}
        result = dedupe_copies([a, b])
        assert len(result) == 1

    def test_different_angle_both_kept(self):
        a = {**GOOD_COPY, "angle": "痛点切入"}
        b = {**GOOD_COPY, "title": "通勤懒人必囤！换季不再干脸", "angle": "避坑型"}
        result = dedupe_copies([a, b])
        assert len(result) == 2

    def test_similar_body_deduped(self):
        a = {**GOOD_COPY}
        b = {**GOOD_COPY, "title": "不一样的标题", "content": GOOD_COPY["content"][:150] + "稍有变动"}
        assert is_similar_copy(a, b) or len(dedupe_copies([a, b])) <= 2


# ── 违禁词三级 ──────────────────────────────────────────────
class TestBannedWordChecker:
    def test_hard_block(self):
        # "美白"已改为 auto_fix（提亮肤色感），hard_block 用速效/医用等词
        result = check_and_fix("速效医用护肤品")
        assert result.status == "hard_block"

    def test_meibaibanned_now_auto_fix(self):
        # Q5对齐：美白→auto_fix 提亮肤色感（不再 hard_block）
        result = check_and_fix("这款美白效果很好")
        assert result.status == "auto_fixed"
        assert result.fixed_text is not None
        assert "美白" not in result.fixed_text

    def test_auto_fix(self):
        result = check_and_fix("这款神器真的好用")
        assert result.status == "auto_fixed"
        assert result.fixed_text is not None
        assert "神器" not in result.fixed_text

    def test_soft_warn(self):
        result = check_and_fix("这绝对是最好的护肤品")
        assert result.status == "soft_warn"

    def test_clean_text_passes(self):
        result = check_and_fix("这款素颜霜轻薄水润，通勤必备。")
        assert result.status == "pass"

    def test_custom_entries_override(self):
        entries = [BannedWordEntry("必备", "hard_block")]
        result = check_and_fix("通勤必备好物", entries)
        assert result.status == "hard_block"


# ── storyboard ─────────────────────────────────────────────
class TestStoryboard:
    def test_clamp_count(self):
        assert clamp_count(0) == 1
        assert clamp_count(9) == 8
        assert clamp_count(3) == 3

    def test_narrative_roles_3(self):
        roles = get_narrative_roles(3)
        assert len(roles) == 3
        assert roles[0]["role"] == "hook"
        assert roles[-1]["role"] == "closer"

    def test_narrative_roles_6(self):
        roles = get_narrative_roles(6)
        assert len(roles) == 6
        # Q6对齐北哥套路：①hook ②product_closeup(单品特写) ③ingredient ④texture ⑤applied_proof ⑥closer
        assert roles[0]["role"] == "hook"
        assert roles[1]["role"] == "product_closeup"
        assert roles[2]["role"] == "ingredient"
        assert roles[4]["role"] == "applied_proof"
        assert roles[5]["role"] == "closer"

    def test_narrative_roles_8(self):
        roles = get_narrative_roles(8)
        assert len(roles) == 8

    def test_plan_image_set_structure(self):
        plan = plan_image_set(GOOD_COPY, PRODUCT, image_count=3)
        assert "storyboard" in plan
        assert "base_prompt" in plan
        assert len(plan["storyboard"]) == 3
        # 产品图锚点说明必须在 base_prompt 中
        assert "不可修改" in plan["base_prompt"]


# ── 飞轮聚合 ───────────────────────────────────────────────
class TestPreferenceAggregator:
    def test_cold_start_when_few_events(self):
        result = aggregate_preference_context([], PRODUCT, workspace_id=1, product_id=1)
        assert result["injected_count"] == 0
        assert "冷启动" in result["recent_preference"]

    def test_aggregate_top_angles(self):
        events = [
            {"signal_type": "text_select", "workspace_id": 1, "product_id": 1, "angle_label": "痛点切入", "signal_weight": 3, "reason": ""},
            {"signal_type": "text_select", "workspace_id": 1, "product_id": 1, "angle_label": "痛点切入", "signal_weight": 3, "reason": ""},
            {"signal_type": "approve",     "workspace_id": 1, "product_id": 1, "angle_label": "场景型",  "signal_weight": 5, "reason": ""},
            {"signal_type": "text_select", "workspace_id": 1, "product_id": 1, "angle_label": "避坑型",  "signal_weight": 3, "reason": ""},
            {"signal_type": "text_select", "workspace_id": 1, "product_id": 1, "angle_label": "痛点切入", "signal_weight": 3, "reason": ""},
            {"signal_type": "text_select", "workspace_id": 1, "product_id": 1, "angle_label": "避坑型",  "signal_weight": 3, "reason": ""},
        ]
        result = aggregate_preference_context(events, PRODUCT, workspace_id=1, product_id=1)
        assert result["injected_count"] == 6
        assert "痛点切入" in result["recent_preference"]
        assert "偏好角度" in result["prompt_fragment"]

    def test_reject_reason_in_prompt(self):
        events = [
            {"signal_type": "reject_with_reason", "workspace_id": 1, "product_id": 1,
             "angle_label": "", "signal_weight": -3, "reason": "标题太硬广"},
            *[{"signal_type": "text_select", "workspace_id": 1, "product_id": 1,
               "angle_label": "痛点切入", "signal_weight": 3, "reason": ""} for _ in range(5)],
        ]
        result = aggregate_preference_context(events, PRODUCT, workspace_id=1, product_id=1)
        assert "标题太硬广" in result["prompt_fragment"]

    def test_product_isolation(self):
        """不同 product_id 的事件不会混进来"""
        events = [
            {"signal_type": "text_select", "workspace_id": 1, "product_id": 2,
             "angle_label": "成分党", "signal_weight": 3, "reason": ""},
            *[{"signal_type": "text_select", "workspace_id": 1, "product_id": 1,
               "angle_label": "场景型", "signal_weight": 3, "reason": ""} for _ in range(5)],
        ]
        result = aggregate_preference_context(events, PRODUCT, workspace_id=1, product_id=1)
        assert "成分党" not in result["prompt_fragment"]

    def test_collect_event_structure(self):
        event = collect_preference_event("text_select", user_id=1, workspace_id=1, product_id=1, angle_label="痛点切入")
        assert event["signal_weight"] == 3
        assert event["data_ownership"] == "client_data"


# ── prompt_composer ────────────────────────────────────────
class TestPromptComposer:
    def test_compose_variants_returns_two_strings(self):
        sys_p, user_p = compose_variants(PRODUCT, count=5)
        assert isinstance(sys_p, str) and len(sys_p) > 0
        assert isinstance(user_p, str) and len(user_p) > 0

    def test_compose_variants_includes_product_name(self):
        _, user_p = compose_variants(PRODUCT, count=3)
        assert "倍分子素颜霜" in user_p

    def test_compose_variants_includes_count(self):
        _, user_p = compose_variants(PRODUCT, count=7)
        assert "7" in user_p

    def test_compose_variants_flywheel_injected(self):
        fragment = "偏好角度参考：场景型、痛点切入"
        _, user_p = compose_variants(PRODUCT, count=3, flywheel_context=fragment)
        assert "场景型" in user_p

    def test_compose_preference_context_delegates(self):
        # compose_preference_context 应委托给 aggregate_preference_context
        events = [
            {"signal_type": "text_select", "workspace_id": 1, "product_id": 1,
             "angle_label": "场景型", "signal_weight": 3, "reason": ""}
            for _ in range(6)
        ]
        result = compose_preference_context(events, PRODUCT, workspace_id=1, product_id=1)
        assert "injected_count" in result
        assert result["injected_count"] == 6
        assert "prompt_fragment" in result

    def test_parse_model_output_valid_json(self):
        raw = '[{"title":"标题1","content":"正文","tags":[],"angle":"场景型"}]'
        parsed = parse_model_output(raw)
        assert len(parsed) == 1 and parsed[0]["title"] == "标题1"

    def test_parse_model_output_markdown_wrapped(self):
        raw = "```json\n[{\"title\":\"a\",\"content\":\"b\",\"tags\":[]}]\n```"
        parsed = parse_model_output(raw)
        assert len(parsed) == 1

    def test_compose_image_prompt_includes_role_and_product(self):
        vs = {"style": "ins摆拍风", "color_palette": "米白+杏色", "base_prompt": "产品近景"}
        prompt = compose_image_prompt("hook", vs, PRODUCT)
        assert "hook" in prompt
        assert "倍分子素颜霜" in prompt
        assert "ins摆拍风" in prompt
