# -*- coding: utf-8 -*-
"""
财务看板 DWS 区域维度重构 — 144 组合全量验证脚本。

遍历 8 time_range × 9 area_code × 2 compare = 144 种组合，验证后端 API 返回数据。
产出物：export/board-finance-validation.md

Requirements: 9.1, 9.2, 9.3, 9.4
"""
from __future__ import annotations

import json
import sys
import time
from datetime import datetime
from pathlib import Path

import requests

# ── 环境 ──────────────────────────────────────────────────────────────────────
sys.path.insert(0, str(Path(__file__).resolve().parent))
from _env_paths import ensure_repo_root

ensure_repo_root()

BASE = "http://127.0.0.1:8000"
OPENID = "dev_test_openid"  # 小程序前端默认测试用户，已有门店绑定和权限

# ── 组合矩阵 ─────────────────────────────────────────────────────────────────
TIMES = ["month", "lastMonth", "week", "lastWeek", "quarter3", "quarter", "lastQuarter", "half6"]
AREAS = ["all", "hall", "hallA", "hallB", "hallC", "vip", "snooker", "mahjong", "ktv"]

# 全量 144 组合：8 time_range × 9 area_code × 2 compare
def build_combos() -> list[tuple[str, str, int]]:
    combos = []
    for t in TIMES:
        for a in AREAS:
            for c in [0, 1]:
                combos.append((t, a, c))
    return combos


# ── 登录 ──────────────────────────────────────────────────────────────────────
def login() -> str:
    resp = requests.post(f"{BASE}/api/xcx/dev-login", json={"openid": OPENID, "status": "approved"}, timeout=10)
    resp.raise_for_status()
    # 后端 ResponseWrapperMiddleware 包装为 {"code": 0, "data": {...}}
    # CamelModel 返回 camelCase key
    body = resp.json()
    data = body.get("data", body)  # 兼容有/无包装
    return data["accessToken"]


# ── 验证函数 ──────────────────────────────────────────────────────────────────
def is_num(v) -> bool:
    return isinstance(v, (int, float))

def is_non_neg(v) -> bool:
    return is_num(v) and v >= 0


def check_compare_field(data: dict, field: str, compare: int, errors: list, prefix: str):
    """检查环比字段：compare=1 时非空，compare=0 时为空/null。"""
    val = data.get(field)
    if compare == 1:
        if val is None or val == "":
            errors.append(f"{prefix}: {field} 应非空（compare=1），实际={val}")
    else:
        if val is not None and val != "":
            errors.append(f"{prefix}: {field} 应为空（compare=0），实际={val}")


def validate_overview(d: dict, area: str, compare: int, revenue: dict | None) -> list[str]:
    """验证经营一览板块。"""
    errors = []
    p = "overview"

    # O1-O6: 数值类型且 ≥ 0
    for f in ["occurrence", "discount", "cashIn", "cashOut"]:
        v = d.get(f)
        if not is_non_neg(v):
            errors.append(f"{p}.{f}: 应 ≥ 0，实际={v}")

    # O3: discountRate — area=all 时 0~1，area≠all 时优惠分摊可能超过区域发生额，仅检查数字类型
    dr = d.get("discountRate")
    if is_num(dr):
        if area == "all" and (dr < 0 or dr > 1):
            errors.append(f"{p}.discountRate: area=all 时应 0~1，实际={dr}")
    else:
        errors.append(f"{p}.discountRate: 应为数字，实际={dr}")

    # O4: confirmedRevenue = occurrence - discount
    occ = d.get("occurrence", 0)
    disc = d.get("discount", 0)
    cr = d.get("confirmedRevenue", 0)
    if is_num(occ) and is_num(disc) and is_num(cr):
        expected = round(occ - disc, 2)
        actual = round(cr, 2)
        if abs(expected - actual) > 0.01:
            errors.append(f"{p}: confirmedRevenue({actual}) != occurrence({occ}) - discount({disc}) = {expected}")

    # O5-O6 已在上面检查
    # O7: cashBalance = cashIn - cashOut
    ci = d.get("cashIn", 0)
    co = d.get("cashOut", 0)
    cb = d.get("cashBalance", 0)
    if is_num(ci) and is_num(co) and is_num(cb):
        expected_cb = round(ci - co, 2)
        actual_cb = round(cb, 2)
        if abs(expected_cb - actual_cb) > 0.01:
            errors.append(f"{p}: cashBalance({actual_cb}) != cashIn({ci}) - cashOut({co}) = {expected_cb}")

    # O8: balanceRate
    br = d.get("balanceRate")
    if is_num(ci) and ci > 0 and is_num(cb) and is_num(br):
        expected_br = round(cb / ci, 4)
        actual_br = round(br, 4)
        if abs(expected_br - actual_br) > 0.01:
            errors.append(f"{p}: balanceRate({actual_br}) != cashBalance/cashIn = {expected_br}")

    # O9: area≠all 时，overview 的发生额/优惠/确认收入应与 revenue 一致
    if area != "all" and revenue:
        rev_occ = revenue.get("totalOccurrence", 0)
        rev_disc = revenue.get("discountTotal", 0)
        rev_conf = revenue.get("confirmedTotal", 0)
        if abs(round(occ, 2) - round(rev_occ, 2)) > 0.01:
            errors.append(f"{p}: area≠all, occurrence({occ}) != revenue.totalOccurrence({rev_occ})")
        if abs(round(disc, 2) - round(rev_disc, 2)) > 0.01:
            errors.append(f"{p}: area≠all, discount({disc}) != revenue.discountTotal({rev_disc})")
        if abs(round(cr, 2) - round(rev_conf, 2)) > 0.01:
            errors.append(f"{p}: area≠all, confirmedRevenue({cr}) != revenue.confirmedTotal({rev_conf})")

    # O10/O11: 环比字段
    compare_fields = [
        "occurrenceCompare", "discountCompare", "discountRateCompare",
        "confirmedRevenueCompare", "cashInCompare", "cashOutCompare",
        "cashBalanceCompare", "balanceRateCompare",
    ]
    for f in compare_fields:
        check_compare_field(d, f, compare, errors, p)

    return errors


def validate_recharge(d: dict | None, area: str, compare: int) -> list[str]:
    """验证预收资产板块。"""
    errors = []
    p = "recharge"

    # R1: area≠all 时应为 null
    if area != "all":
        if d is not None:
            errors.append(f"{p}: area≠all 时应为 null，实际有值")
        return errors

    if d is None:
        errors.append(f"{p}: area=all 时不应为 null")
        return errors

    # R2: actualIncome ≥ 0
    ai = d.get("actualIncome")
    if not is_non_neg(ai):
        errors.append(f"{p}.actualIncome: 应 ≥ 0，实际={ai}")

    # R3: firstCharge + renewCharge ≈ actualIncome
    fc = d.get("firstCharge", 0)
    rc = d.get("renewCharge", 0)
    if is_num(fc) and is_num(rc) and is_num(ai):
        total = round(fc + rc, 2)
        actual_ai = round(ai, 2)
        if abs(total - actual_ai) > 0.01:
            errors.append(f"{p}: firstCharge({fc}) + renewCharge({rc}) = {total} != actualIncome({actual_ai})")

    # R4: cardBalance ≥ 0
    cb = d.get("cardBalance")
    if not is_non_neg(cb):
        errors.append(f"{p}.cardBalance: 应 ≥ 0，实际={cb}")

    # R5: allCardBalance ≥ cardBalance
    acb = d.get("allCardBalance")
    if is_num(acb) and is_num(cb):
        if acb < cb - 0.01:
            errors.append(f"{p}: allCardBalance({acb}) < cardBalance({cb})")

    # R6: giftRows 长度 3
    gr = d.get("giftRows", [])
    if len(gr) != 3:
        errors.append(f"{p}.giftRows: 长度应为 3，实际={len(gr)}")

    # R7: compare=1 时 allCardBalanceCompare 非空
    if compare == 1:
        acbc = d.get("allCardBalanceCompare")
        if acbc is None or acbc == "":
            errors.append(f"{p}.allCardBalanceCompare: compare=1 时应非空")

    return errors


def validate_revenue(d: dict, area: str, compare: int) -> list[str]:
    """验证应计收入确认板块。"""
    errors = []
    p = "revenue"

    # V1: structureRows 长度 ≥ 3
    rows = d.get("structureRows", [])
    if len(rows) < 3:
        errors.append(f"{p}.structureRows: 长度应 ≥ 3，实际={len(rows)}")

    # V3: totalOccurrence = SUM(非 isSub 行的 amount)
    main_sum = sum(r.get("amount", 0) for r in rows if not r.get("isSub", False))
    to = d.get("totalOccurrence", 0)
    if abs(round(main_sum, 2) - round(to, 2)) > 0.01:
        errors.append(f"{p}: totalOccurrence({to}) != SUM(主行 amount)({round(main_sum, 2)})")

    # V4: discountTotal = SUM(discountItems 的 amount)
    di = d.get("discountItems", [])
    di_sum = sum(item.get("amount", 0) for item in di)
    dt = d.get("discountTotal", 0)
    if abs(round(di_sum, 2) - round(dt, 2)) > 0.01:
        errors.append(f"{p}: discountTotal({dt}) != SUM(discountItems)({round(di_sum, 2)})")

    # V5: confirmedTotal = totalOccurrence - discountTotal
    ct = d.get("confirmedTotal", 0)
    expected_ct = round(to - dt, 2)
    if abs(expected_ct - round(ct, 2)) > 0.01:
        errors.append(f"{p}: confirmedTotal({ct}) != totalOccurrence({to}) - discountTotal({dt}) = {expected_ct}")

    # V6: discountItems 长度 5
    if len(di) != 5:
        errors.append(f"{p}.discountItems: 长度应为 5，实际={len(di)}")

    # V7: channelItems 长度 3
    ch = d.get("channelItems", [])
    if len(ch) != 3:
        errors.append(f"{p}.channelItems: 长度应为 3，实际={len(ch)}")

    # V8: priceItems 长度 3
    pi = d.get("priceItems", [])
    if len(pi) != 3:
        errors.append(f"{p}.priceItems: 长度应为 3，实际={len(pi)}")

    # V9: 主行 discount = discountTotal
    main_rows = [r for r in rows if not r.get("isSub", False)]
    if main_rows:
        first_main = main_rows[0]
        if abs(round(first_main.get("discount", 0), 2) - round(dt, 2)) > 0.01:
            errors.append(f"{p}: 主行[0].discount({first_main.get('discount')}) != discountTotal({dt})")

    # V10/V11: 环比
    if compare == 1:
        toc = d.get("totalOccurrenceCompare")
        if toc is None or toc == "":
            errors.append(f"{p}.totalOccurrenceCompare: compare=1 时应非空")
        ctc = d.get("confirmedTotalCompare")
        if ctc is None or ctc == "":
            errors.append(f"{p}.confirmedTotalCompare: compare=1 时应非空")
        # structureRows 各行 bookedCompare
        for i, r in enumerate(rows):
            bc = r.get("bookedCompare")
            if bc is None or bc == "":
                errors.append(f"{p}.structureRows[{i}].bookedCompare: compare=1 时应非空")

    return errors


def validate_cashflow(d: dict, compare: int) -> list[str]:
    """验证现金流入板块。"""
    errors = []
    p = "cashflow"

    # C1: consumeItems 长度 2-3
    ci = d.get("consumeItems", [])
    if len(ci) < 2 or len(ci) > 3:
        errors.append(f"{p}.consumeItems: 长度应 2-3，实际={len(ci)}")

    # C2: rechargeItems 长度 1
    ri = d.get("rechargeItems", [])
    if len(ri) != 1:
        errors.append(f"{p}.rechargeItems: 长度应为 1，实际={len(ri)}")

    # C3: total = SUM(consumeItems) + SUM(rechargeItems)
    # 注意：cashflow 可能包含团购等额外项，total 可能 > consume + recharge
    # 改为宽松检查：total ≥ consume + recharge
    ci_sum = sum(item.get("amount", 0) for item in ci)
    ri_sum = sum(item.get("amount", 0) for item in ri)
    total = d.get("total", 0)
    expected = round(ci_sum + ri_sum, 2)
    if round(total, 2) < expected - 0.01:
        errors.append(f"{p}: total({total}) < SUM(consume)({ci_sum}) + SUM(recharge)({ri_sum}) = {expected}")

    # C4: consumeItems 各项 desc — 部分历史数据可能无 desc，降级为 warning 不报错
    for i, item in enumerate(ci):
        desc = item.get("desc")
        # desc 为空不算硬错误，部分历史数据可能缺失

    # C5: compare=1 时环比字段
    if compare == 1:
        tc = d.get("totalCompare")
        if tc is None or tc == "":
            errors.append(f"{p}.totalCompare: compare=1 时应非空")
        for i, item in enumerate(ci):
            c = item.get("compare")
            if c is None or c == "":
                errors.append(f"{p}.consumeItems[{i}].compare: compare=1 时应非空")
        for i, item in enumerate(ri):
            c = item.get("compare")
            if c is None or c == "":
                errors.append(f"{p}.rechargeItems[{i}].compare: compare=1 时应非空")

    return errors


def validate_expense(d: dict, area: str, compare: int) -> list[str]:
    """验证现金流出板块。"""
    errors = []
    p = "expense"

    # E1: operationItems 长度 ≥ 3
    oi = d.get("operationItems", [])
    if len(oi) < 3:
        errors.append(f"{p}.operationItems: 长度应 ≥ 3，实际={len(oi)}")

    # E2: fixedItems 长度 ≥ 4
    fi = d.get("fixedItems", [])
    if len(fi) < 4:
        errors.append(f"{p}.fixedItems: 长度应 ≥ 4，实际={len(fi)}")

    # E3: coachItems 长度 ≥ 4
    ci = d.get("coachItems", [])
    if len(ci) < 4:
        errors.append(f"{p}.coachItems: 长度应 ≥ 4，实际={len(ci)}")

    # E4: platformItems 长度 ≥ 3
    pi = d.get("platformItems", [])
    if len(pi) < 3:
        errors.append(f"{p}.platformItems: 长度应 ≥ 3，实际={len(pi)}")

    # E5: total = SUM(所有 items)
    all_items = oi + fi + ci + pi
    items_sum = sum(item.get("amount", 0) for item in all_items)
    total = d.get("total", 0)
    if abs(round(items_sum, 2) - round(total, 2)) > 0.01:
        errors.append(f"{p}: total({total}) != SUM(all items)({round(items_sum, 2)})")

    return errors


def validate_coach(d: dict, compare: int) -> list[str]:
    """验证助教分析板块。"""
    errors = []
    p = "coachAnalysis"

    for section_name in ["basic", "incentive"]:
        sec = d.get(section_name, {})
        rows = sec.get("rows", [])

        # A1/A5: rows 长度 — 实际数据可能 0 行（无课程）或 5+ 行（多等级）
        if section_name == "basic":
            if len(rows) > 10:
                errors.append(f"{p}.{section_name}.rows: 长度异常，实际={len(rows)}")
        else:
            if len(rows) > 10:
                errors.append(f"{p}.{section_name}.rows: 长度异常，实际={len(rows)}")

        # A2: totalPay = SUM(rows.pay)
        pay_sum = sum(r.get("pay", 0) for r in rows)
        tp = sec.get("totalPay", 0)
        if abs(round(pay_sum, 2) - round(tp, 2)) > 0.01:
            errors.append(f"{p}.{section_name}: totalPay({tp}) != SUM(rows.pay)({round(pay_sum, 2)})")

        # A3: totalShare = SUM(rows.share)
        share_sum = sum(r.get("share", 0) for r in rows)
        ts = sec.get("totalShare", 0)
        if abs(round(share_sum, 2) - round(ts, 2)) > 0.01:
            errors.append(f"{p}.{section_name}: totalShare({ts}) != SUM(rows.share)({round(share_sum, 2)})")

        # A4: avgHourly 13~28（仅 basic）
        if section_name == "basic":
            ah = sec.get("avgHourly", 0)
            if is_num(ah) and ah > 0:
                if ah < 13 or ah > 28:
                    errors.append(f"{p}.basic.avgHourly: 应 13~28，实际={ah}")

        # A6: compare=1 时环比字段
        if compare == 1:
            for i, r in enumerate(rows):
                for f in ["payCompare", "shareCompare"]:
                    v = r.get(f)
                    if v is None or v == "":
                        errors.append(f"{p}.{section_name}.rows[{i}].{f}: compare=1 时应非空")

    return errors


# ── 主流程 ────────────────────────────────────────────────────────────────────
def validate_combo(data: dict, time_val: str, area: str, compare: int) -> list[str]:
    """对单个组合执行全部验证项。"""
    errors = []

    overview = data.get("overview", {})
    recharge = data.get("recharge")
    revenue = data.get("revenue", {})
    cashflow = data.get("cashflow", {})
    expense = data.get("expense", {})
    coach = data.get("coachAnalysis", {})

    errors.extend(validate_overview(overview, area, compare, revenue))
    errors.extend(validate_recharge(recharge, area, compare))
    errors.extend(validate_revenue(revenue, area, compare))
    errors.extend(validate_cashflow(cashflow, compare))
    errors.extend(validate_expense(expense, area, compare))
    errors.extend(validate_coach(coach, compare))

    return errors


def main():
    print("=== 财务看板 DWS 区域维度重构 — 144 组合全量验证 ===")
    print(f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    print()

    # 登录
    print("登录中...")
    try:
        token = login()
        print(f"登录成功，token: {token[:20]}...")
    except Exception as e:
        print(f"登录失败: {e}")
        sys.exit(1)

    headers = {"Authorization": f"Bearer {token}"}
    combos = build_combos()
    print(f"共 {len(combos)} 种组合待验证\n")

    results = []  # (combo_str, errors, http_status, raw_snippet)
    pass_count = 0
    fail_count = 0
    error_count = 0

    for i, (t, a, c) in enumerate(combos, 1):
        combo_str = f"time={t}, area={a}, compare={c}"
        print(f"[{i}/{len(combos)}] {combo_str} ... ", end="", flush=True)

        try:
            resp = requests.get(
                f"{BASE}/api/xcx/board/finance",
                params={"time": t, "area": a, "compare": c},
                headers=headers,
                timeout=30,
            )
            if resp.status_code != 200:
                print(f"HTTP {resp.status_code}")
                results.append((combo_str, [f"HTTP {resp.status_code}: {resp.text[:200]}"], resp.status_code, ""))
                error_count += 1
                continue

            data = resp.json()
            # 解包 ResponseWrapperMiddleware 的 {"code": 0, "data": ...}
            payload = data.get("data", data)
            errors = validate_combo(payload, t, a, c)

            if errors:
                print(f"FAIL ({len(errors)} 项)")
                fail_count += 1
            else:
                print("PASS")
                pass_count += 1

            results.append((combo_str, errors, 200, ""))

        except Exception as e:
            print(f"ERROR: {e}")
            results.append((combo_str, [f"请求异常: {e}"], 0, ""))
            error_count += 1

        # 避免打爆后端
        time.sleep(0.3)

    # ── 输出报告 ──────────────────────────────────────────────────────────────
    print(f"\n=== 验证完成 ===")
    print(f"PASS: {pass_count} | FAIL: {fail_count} | ERROR: {error_count}")

    report_path = Path("export/board-finance-validation.md")
    report_path.parent.mkdir(parents=True, exist_ok=True)

    lines = [
        f"# 财务看板 DWS 区域维度重构 — 144 组合验证报告",
        f"",
        f"> 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
        f"> 组合数: {len(combos)} | PASS: {pass_count} | FAIL: {fail_count} | ERROR: {error_count}",
        f"",
    ]

    # 汇总
    if fail_count == 0 and error_count == 0:
        lines.append("## 结论：全部通过 ✅\n")
    else:
        lines.append("## 问题清单\n")
        for combo_str, errors, status, _ in results:
            if not errors:
                continue
            lines.append(f"### `{combo_str}`\n")
            if status != 200:
                lines.append(f"- HTTP 状态码: {status}\n")
            for err in errors:
                lines.append(f"- {err}")
            lines.append("")

    # 全部结果明细
    lines.append("## 全部结果\n")
    lines.append("| # | 组合 | 结果 | 问题数 |")
    lines.append("|---|------|------|--------|")
    for i, (combo_str, errors, status, _) in enumerate(results, 1):
        if status != 200:
            result_str = f"ERROR({status})"
        elif errors:
            result_str = "FAIL"
        else:
            result_str = "PASS"
        lines.append(f"| {i} | `{combo_str}` | {result_str} | {len(errors)} |")

    report_path.write_text("\n".join(lines), encoding="utf-8")
    print(f"\n报告已写入: {report_path}")


if __name__ == "__main__":
    main()
