"""
单元测试 — V4 compute_signal_scores

5 条 mock 数据覆盖关键场景：
  1. 严重衰退老广告（高 decay_score）
  2. 边缘衰退（中等 decay_score）
  3. 高 ROI 低消耗（高 bid_up_score）
  4. ROI 同群下移（高 bid_down_score）
  5. 新广告冷启动（所有评分都低）

运行：
  cd /Users/liulidong/project/agent/Agent
  .venv/bin/python3 examples/auto_put_ad_mini/test_compute_signal_scores.py
"""
import sys
from pathlib import Path

# 路径设置
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))

import pandas as pd

from examples.auto_put_ad_mini.tools.ad_decision import compute_signal_scores


def _make_context():
    """统一的 context（含 tier baseline + global baseline + market_signal）"""
    return {
        "by_audience_tier_baseline": {
            "R330": {"roi_p25": 1.50, "roi_p50": 1.90, "roi_p75": 2.40},
            "R500": {"roi_p25": 1.20, "roi_p50": 1.60, "roi_p75": 2.10},
            "R50":  {"roi_p25": 2.20, "roi_p50": 2.80, "roi_p75": 3.50},
        },
        "global_baseline": {"roi_p25": 1.50, "roi_p50": 2.00, "roi_p75": 2.60},
        "cost_7d_avg_median": 200.0,
        "market_signal": "stable",
    }


def _print_score(name, expected, actual_pack):
    print(f"\n--- {name} ---")
    print(f"  decay_score: {actual_pack['decay_score']}")
    print(f"  decay_signals: {actual_pack['decay_signals']}")
    print(f"  bid_down_score: {actual_pack['bid_down_score']} (scenario={actual_pack['bid_down_scenario']}, sugg={actual_pack['bid_down_suggested_pct']})")
    print(f"  bid_up_score: {actual_pack['bid_up_score']} (label={actual_pack['bid_up_label']}, sugg={actual_pack['bid_up_suggested_pct']})")
    print(f"  rule_recommended_action: {actual_pack['rule_recommended_action']} (basis={actual_pack['rule_recommendation_basis']})")
    for key, expect in expected.items():
        if key == "rule_action":
            actual = actual_pack["rule_recommended_action"]
            ok = actual == expect
        elif key.endswith("_min"):
            base = key.replace("_min", "")
            actual = actual_pack[base]
            ok = actual >= expect
        elif key.endswith("_max"):
            base = key.replace("_max", "")
            actual = actual_pack[base]
            ok = actual <= expect
        else:
            actual = actual_pack[key]
            ok = actual == expect
        status = "[OK]" if ok else "[FAIL]"
        print(f"    {status} {key}: 期望 {expect} 实际 {actual}")
        if not ok:
            return False
    return True


def test_case_1_severe_decay():
    """场景 1：168 天老广告 + 干预 + ROI 低 → decay_score >= 0.55, rule=pause"""
    row = pd.Series({
        "ad_age_days": 168,
        "bid_increased_7d": True,
        "bid_change_pct_7d": 0.08,
        "creatives_added_7d": 2,
        "day_amount_changed_7d": False,
        "stable_spend_days_30d": 18,
        "动态ROI_7日均值": 1.04,
        "cost_7d_avg": 130,
        "relative_trend_pct": -0.21,
        "budget_headroom_pct": 0.50,
        "audience_tier": "R50",
    })
    pack = compute_signal_scores(row, _make_context())
    return _print_score("Case 1 严重衰退", {
        "decay_score_min": 0.55,
        "rule_action": "pause",
    }, pack)


def test_case_2_edge_decay():
    """场景 2：30 天广告 + 已干预 + ROI 略低 → decay_score 0.30~0.55, rule 倾向 bid_down 或 pause"""
    row = pd.Series({
        "ad_age_days": 30,
        "bid_increased_7d": True,
        "bid_change_pct_7d": 0.05,
        "creatives_added_7d": 1,
        "day_amount_changed_7d": False,
        "stable_spend_days_30d": 8,
        "动态ROI_7日均值": 1.40,
        "cost_7d_avg": 150,
        "relative_trend_pct": -0.08,
        "budget_headroom_pct": 0.40,
        "audience_tier": "R330",
    })
    pack = compute_signal_scores(row, _make_context())
    return _print_score("Case 2 边缘衰退", {
        "decay_score_min": 0.30,
        "decay_score_max": 0.65,
    }, pack)


def test_case_3_high_roi_low_cost():
    """场景 3：高 ROI 低消耗 → bid_up_score >= 0.50, rule=bid_up"""
    row = pd.Series({
        "ad_age_days": 30,
        "bid_increased_7d": False,
        "bid_change_pct_7d": 0.0,
        "creatives_added_7d": 0,
        "day_amount_changed_7d": False,
        "stable_spend_days_30d": 5,
        "动态ROI_7日均值": 3.20,  # > R330 p75=2.40
        "cost_7d_avg": 60,        # < median 200 × 0.5
        "relative_trend_pct": 0.10,
        "budget_headroom_pct": 0.50,
        "audience_tier": "R330",
    })
    pack = compute_signal_scores(row, _make_context())
    return _print_score("Case 3 高ROI低量", {
        "bid_up_score_min": 0.50,
        "rule_action": "bid_up",
    }, pack)


def test_case_4_tier_drift():
    """场景 4：ROI 同群下移 → bid_down_score >= 0.40, scenario=tier_drift 或 noise/general"""
    row = pd.Series({
        "ad_age_days": 25,
        "bid_increased_7d": False,
        "bid_change_pct_7d": 0.0,
        "creatives_added_7d": 0,
        "day_amount_changed_7d": False,
        "stable_spend_days_30d": 6,
        "动态ROI_7日均值": 1.30,    # < R330 p25=1.50
        "cost_7d_avg": 200,
        "relative_trend_pct": -0.18,  # 趋势负（同群下移）
        "budget_headroom_pct": 0.40,
        "audience_tier": "R330",
    })
    pack = compute_signal_scores(row, _make_context())
    return _print_score("Case 4 同群下移", {
        "bid_down_score_min": 0.30,
        "rule_action": "bid_down",
    }, pack)


def test_case_5_new_ad():
    """场景 5：新广告冷启动 → 所有评分都低，rule=hold"""
    row = pd.Series({
        "ad_age_days": 3,
        "bid_increased_7d": False,
        "bid_change_pct_7d": 0.0,
        "creatives_added_7d": 0,
        "day_amount_changed_7d": False,
        "stable_spend_days_30d": 0,
        "动态ROI_7日均值": 1.80,
        "cost_7d_avg": 80,
        "relative_trend_pct": None,
        "budget_headroom_pct": 0.30,
        "audience_tier": "R330",
    })
    pack = compute_signal_scores(row, _make_context())
    return _print_score("Case 5 新广告冷启动", {
        "decay_score_max": 0.30,
        "rule_action": "hold",
    }, pack)


def main():
    cases = [
        test_case_1_severe_decay,
        test_case_2_edge_decay,
        test_case_3_high_roi_low_cost,
        test_case_4_tier_drift,
        test_case_5_new_ad,
    ]
    results = [c() for c in cases]
    passed = sum(1 for r in results if r)
    total = len(results)
    print(f"\n{'='*60}")
    print(f"总计: {passed}/{total} 通过")
    print(f"{'='*60}")
    sys.exit(0 if passed == total else 1)


if __name__ == "__main__":
    main()
