# -*- coding: utf-8 -*-
"""助教月度聚合属性测试 — 使用 hypothesis 验证档位分段聚合的正确性。

纯单元测试，模拟 _extract_daily_aggregates() 的 SQL GROUP BY 逻辑，不涉及真实数据库连接。

# Feature: etl-aggregation-fix, Property 3: 档位分段聚合正确性
# **Validates: Requirements 2.1**
"""
from __future__ import annotations

from collections import defaultdict
from datetime import date, timedelta
from decimal import Decimal
from typing import Any, Dict, List, Set, Tuple

from hypothesis import given, settings
from hypothesis import strategies as st


# ── 业绩指标字段列表（与 _extract_daily_aggregates SQL 中 SUM 聚合的字段一致）──

_METRIC_FIELDS = [
    "total_service_count",
    "base_service_count",
    "bonus_service_count",
    "room_service_count",
    "total_hours",
    "base_hours",
    "bonus_hours",
    "room_hours",
    "total_ledger_amount",
    "base_ledger_amount",
    "bonus_ledger_amount",
    "room_ledger_amount",
    "unique_customers",
    "unique_tables",
    "trashed_seconds",
    "trashed_count",
]


# ── 核心逻辑：模拟 _extract_daily_aggregates() 的 SQL GROUP BY 行为 ──
#
# 实际 SQL：
#   SELECT assistant_id, assistant_level_code, assistant_level_name,
#          (ARRAY_AGG(assistant_nickname ORDER BY stat_date DESC))[1] AS assistant_nickname,
#          DATE_TRUNC('month', stat_date)::DATE AS stat_month,
#          COUNT(DISTINCT stat_date) AS work_days,
#          SUM(total_service_count) AS total_service_count,
#          ...
#   FROM dws.dws_assistant_daily_detail
#   WHERE site_id = %s AND (stat_date >= ... AND stat_date < ...)
#   GROUP BY assistant_id, assistant_level_code, assistant_level_name,
#            DATE_TRUNC('month', stat_date)


def simulate_extract_daily_aggregates(
    daily_rows: List[Dict[str, Any]],
    site_id: int,
    month: date,
) -> List[Dict[str, Any]]:
    """模拟 _extract_daily_aggregates() 的 GROUP BY 逻辑。

    按 (assistant_id, assistant_level_code, assistant_level_name, stat_month) 分组，
    对业绩指标做 SUM 聚合，work_days 做 COUNT(DISTINCT stat_date)。
    """
    month_start = month.replace(day=1)
    next_month = (month_start.replace(day=28) + timedelta(days=4)).replace(day=1)

    # 筛选：site_id 匹配 + 日期在月份范围内
    filtered = [
        r for r in daily_rows
        if r.get("site_id") == site_id
        and month_start <= r["stat_date"] < next_month
    ]

    # GROUP BY (assistant_id, assistant_level_code, assistant_level_name)
    groups: Dict[Tuple, List[Dict]] = defaultdict(list)
    for row in filtered:
        key = (
            row["assistant_id"],
            row.get("assistant_level_code"),
            row.get("assistant_level_name"),
        )
        groups[key].append(row)

    results = []
    for (aid, level_code, level_name), rows in groups.items():
        agg: Dict[str, Any] = {
            "assistant_id": aid,
            "assistant_level_code": level_code,
            "assistant_level_name": level_name,
            "stat_month": month_start,
            "work_days": len({r["stat_date"] for r in rows}),
        }
        # SUM 聚合所有业绩指标
        for field in _METRIC_FIELDS:
            agg[field] = sum(Decimal(str(r.get(field, 0))) for r in rows)
        # nickname 按时间倒序取第一条
        sorted_rows = sorted(rows, key=lambda r: r["stat_date"], reverse=True)
        agg["assistant_nickname"] = sorted_rows[0].get("assistant_nickname", "")
        results.append(agg)

    return results


# ── 生成策略 ──────────────────────────────────────────────────

# 助教 ID
_assistant_id_st = st.integers(min_value=1, max_value=50)

# 档位代码：A/B/C/D/E 代表不同档位
_level_code_st = st.sampled_from(["A", "B", "C", "D", "E"])

# 档位名称映射（与 code 绑定，保证同一 code 对应同一 name）
_LEVEL_NAME_MAP = {
    "A": "初级",
    "B": "中级",
    "C": "高级",
    "D": "资深",
    "E": "专家",
}

# 昵称
_nickname_st = st.text(
    alphabet=st.sampled_from("张李王赵刘陈杨黄周吴"),
    min_size=2,
    max_size=4,
)

# 非负业绩指标值（整数型）
_count_metric_st = st.integers(min_value=0, max_value=500)

# 非负业绩指标值（金额/小时型，用 Decimal 表示）
_amount_metric_st = st.decimals(
    min_value=0,
    max_value=Decimal("99999.99"),
    places=2,
    allow_nan=False,
    allow_infinity=False,
)


@st.composite
def _daily_detail_row_st(draw, assistant_id: int, level_code: str, month: date):
    """生成一条 dws_assistant_daily_detail 日度明细行。

    assistant_id 和 level_code 由外部指定，确保可控制分组。
    stat_date 在指定月份内随机选取。
    """
    # 月内随机日期
    month_start = month.replace(day=1)
    next_month = (month_start.replace(day=28) + timedelta(days=4)).replace(day=1)
    max_day = (next_month - timedelta(days=1)).day
    day = draw(st.integers(min_value=1, max_value=max_day))
    stat_date = month_start.replace(day=day)

    return {
        "site_id": 1,  # 固定 site_id 简化测试
        "assistant_id": assistant_id,
        "assistant_level_code": level_code,
        "assistant_level_name": _LEVEL_NAME_MAP[level_code],
        "assistant_nickname": draw(_nickname_st),
        "stat_date": stat_date,
        "total_service_count": draw(_count_metric_st),
        "base_service_count": draw(_count_metric_st),
        "bonus_service_count": draw(_count_metric_st),
        "room_service_count": draw(_count_metric_st),
        "total_hours": float(draw(_amount_metric_st)),
        "base_hours": float(draw(_amount_metric_st)),
        "bonus_hours": float(draw(_amount_metric_st)),
        "room_hours": float(draw(_amount_metric_st)),
        "total_ledger_amount": float(draw(_amount_metric_st)),
        "base_ledger_amount": float(draw(_amount_metric_st)),
        "bonus_ledger_amount": float(draw(_amount_metric_st)),
        "room_ledger_amount": float(draw(_amount_metric_st)),
        "unique_customers": draw(_count_metric_st),
        "unique_tables": draw(_count_metric_st),
        "trashed_seconds": draw(_count_metric_st),
        "trashed_count": draw(_count_metric_st),
    }


@st.composite
def _multi_level_scenario_st(draw):
    """生成一个助教在同一月内有多个档位的场景。

    返回：
    - daily_rows: 日度明细行列表
    - assistant_id: 助教 ID
    - month: 统计月份
    - level_codes: 该助教在该月的不同档位代码集合
    """
    assistant_id = draw(_assistant_id_st)
    # 固定月份为 2025-01
    month = date(2025, 1, 1)

    # 随机选取 1~4 个不同档位
    n_levels = draw(st.integers(min_value=1, max_value=4))
    level_codes = draw(
        st.lists(
            _level_code_st,
            min_size=n_levels,
            max_size=n_levels,
            unique=True,
        )
    )

    # 为每个档位生成 1~5 条日度明细
    daily_rows: List[Dict[str, Any]] = []
    for code in level_codes:
        n_rows = draw(st.integers(min_value=1, max_value=5))
        for _ in range(n_rows):
            row = draw(_daily_detail_row_st(assistant_id, code, month))
            daily_rows.append(row)

    return {
        "daily_rows": daily_rows,
        "assistant_id": assistant_id,
        "month": month,
        "level_codes": set(level_codes),
    }


# ── Property 3: 档位分段聚合正确性 ───────────────────────────
# Feature: etl-aggregation-fix, Property 3: 档位分段聚合正确性
# **Validates: Requirements 2.1**
#
# 对于任意助教在同一月内存在 N 个不同 assistant_level_code 的日度数据，
# _extract_daily_aggregates() 应返回恰好 N 行记录，
# 每行的业绩指标之和应等于该助教该月的总业绩。


class TestProperty3LevelSegmentAggregation:
    """Property 3: 档位分段聚合正确性。"""

    @given(scenario=_multi_level_scenario_st())
    @settings(max_examples=200)
    def test_row_count_equals_distinct_level_codes(self, scenario):
        """聚合结果行数应等于不同 assistant_level_code 的数量。"""
        daily_rows = scenario["daily_rows"]
        assistant_id = scenario["assistant_id"]
        month = scenario["month"]
        level_codes = scenario["level_codes"]

        results = simulate_extract_daily_aggregates(daily_rows, site_id=1, month=month)

        # 只看当前助教的结果
        assistant_results = [r for r in results if r["assistant_id"] == assistant_id]

        assert len(assistant_results) == len(level_codes), (
            f"助教 {assistant_id} 有 {len(level_codes)} 个不同档位 {level_codes}，"
            f"但聚合结果返回 {len(assistant_results)} 行"
        )

    @given(scenario=_multi_level_scenario_st())
    @settings(max_examples=200)
    def test_metric_sums_equal_total(self, scenario):
        """所有档位行的业绩指标之和应等于该助教该月的总业绩。"""
        daily_rows = scenario["daily_rows"]
        assistant_id = scenario["assistant_id"]
        month = scenario["month"]

        results = simulate_extract_daily_aggregates(daily_rows, site_id=1, month=month)
        assistant_results = [r for r in results if r["assistant_id"] == assistant_id]

        # 手动计算该助教该月的总业绩（不分档位）
        for field in _METRIC_FIELDS:
            expected_total = sum(
                Decimal(str(r.get(field, 0)))
                for r in daily_rows
                if r["assistant_id"] == assistant_id
            )
            actual_total = sum(r[field] for r in assistant_results)

            assert actual_total == expected_total, (
                f"指标 {field}: 各档位之和={actual_total} != 总业绩={expected_total}"
            )

    @given(scenario=_multi_level_scenario_st())
    @settings(max_examples=200)
    def test_each_row_has_correct_level_code(self, scenario):
        """每行结果的 assistant_level_code 应属于原始数据中出现的档位集合。"""
        daily_rows = scenario["daily_rows"]
        assistant_id = scenario["assistant_id"]
        month = scenario["month"]
        level_codes = scenario["level_codes"]

        results = simulate_extract_daily_aggregates(daily_rows, site_id=1, month=month)
        assistant_results = [r for r in results if r["assistant_id"] == assistant_id]

        result_codes = {r["assistant_level_code"] for r in assistant_results}
        assert result_codes == level_codes, (
            f"聚合结果的档位集合 {result_codes} != 原始数据的档位集合 {level_codes}"
        )

    @given(scenario=_multi_level_scenario_st())
    @settings(max_examples=100)
    def test_per_level_metrics_match_source(self, scenario):
        """每个档位行的业绩指标应等于该档位对应日度数据的 SUM。"""
        daily_rows = scenario["daily_rows"]
        assistant_id = scenario["assistant_id"]
        month = scenario["month"]

        results = simulate_extract_daily_aggregates(daily_rows, site_id=1, month=month)
        assistant_results = {
            r["assistant_level_code"]: r
            for r in results
            if r["assistant_id"] == assistant_id
        }

        # 按档位分组计算期望值
        by_level: Dict[str, List[Dict]] = defaultdict(list)
        for r in daily_rows:
            if r["assistant_id"] == assistant_id:
                by_level[r["assistant_level_code"]].append(r)

        for code, rows in by_level.items():
            assert code in assistant_results, (
                f"档位 {code} 在原始数据中存在但聚合结果中缺失"
            )
            agg = assistant_results[code]
            for field in _METRIC_FIELDS:
                expected = sum(Decimal(str(r.get(field, 0))) for r in rows)
                assert agg[field] == expected, (
                    f"档位 {code} 指标 {field}: 聚合值={agg[field]} != 期望={expected}"
                )


# ── Property 4: nickname 按时间倒序取值 ─────────────────────────
# Feature: etl-aggregation-fix, Property 4: nickname 按时间倒序取值
# **Validates: Requirements 2.3, 2.5, 2.6**
#
# 对于任意助教在聚合周期内有多条不同 nickname 的记录，
# 聚合结果中的 nickname 应等于时间最晚的那条记录的 nickname。
# 此属性适用于 AssistantMonthlyTask、AssistantFinanceTask、AssistantCustomerTask。


# ── 模拟函数：FinanceTask._extract_daily_revenue() 的 nickname 取值逻辑 ──
#
# 实际 SQL：
#   SELECT DATE(s.start_use_time) AS stat_date,
#          s.site_assistant_id AS assistant_id,
#          (ARRAY_AGG(s.nickname ORDER BY s.start_use_time DESC))[1] AS assistant_nickname,
#          ...
#   GROUP BY DATE(s.start_use_time), s.site_assistant_id


def simulate_finance_daily_revenue(
    service_rows: List[Dict[str, Any]],
    site_id: int,
    start_date: date,
    end_date: date,
) -> List[Dict[str, Any]]:
    """模拟 AssistantFinanceTask._extract_daily_revenue() 的 GROUP BY 逻辑。

    按 (stat_date, assistant_id) 分组，nickname 按 start_use_time 倒序取第一条。
    """
    filtered = [
        r for r in service_rows
        if r.get("site_id") == site_id
        and start_date <= r["stat_date"] <= end_date
        and r.get("is_delete", 0) == 0
    ]

    groups: Dict[Tuple, List[Dict]] = defaultdict(list)
    for row in filtered:
        key = (row["stat_date"], row["assistant_id"])
        groups[key].append(row)

    results = []
    for (stat_date, aid), rows in groups.items():
        # nickname 按 start_use_time 倒序取第一条
        sorted_rows = sorted(rows, key=lambda r: r["start_use_time"], reverse=True)
        results.append({
            "stat_date": stat_date,
            "assistant_id": aid,
            "assistant_nickname": sorted_rows[0].get("nickname", ""),
        })
    return results


# ── 模拟函数：CustomerTask._extract_service_pairs() 的 nickname 取值逻辑 ──
#
# 实际 SQL：
#   SELECT assistant_id,
#          (ARRAY_AGG(assistant_nickname ORDER BY service_date DESC))[1] AS assistant_nickname,
#          member_id, ...
#   GROUP BY assistant_id, member_id


def simulate_customer_service_pairs(
    service_rows: List[Dict[str, Any]],
    site_id: int,
) -> List[Dict[str, Any]]:
    """模拟 AssistantCustomerTask._extract_service_pairs() 的 GROUP BY 逻辑。

    按 (assistant_id, member_id) 分组，nickname 按 service_date 倒序取第一条。
    """
    filtered = [
        r for r in service_rows
        if r.get("site_id") == site_id
        and r.get("member_id") is not None
        and r.get("member_id") != 0
        and r.get("is_delete", 0) == 0
    ]

    groups: Dict[Tuple, List[Dict]] = defaultdict(list)
    for row in filtered:
        key = (row["assistant_id"], row["member_id"])
        groups[key].append(row)

    results = []
    for (aid, mid), rows in groups.items():
        sorted_rows = sorted(rows, key=lambda r: r["service_date"], reverse=True)
        results.append({
            "assistant_id": aid,
            "member_id": mid,
            "assistant_nickname": sorted_rows[0].get("assistant_nickname", ""),
        })
    return results


# ── Property 4 生成策略 ──────────────────────────────────────

# 确保同一分组内有多条不同 nickname 的记录
_distinct_nickname_st = st.lists(
    st.text(
        alphabet=st.sampled_from("张李王赵刘陈杨黄周吴徐孙马朱胡林郭何高"),
        min_size=2,
        max_size=4,
    ),
    min_size=2,
    max_size=6,
    unique=True,
)


@st.composite
def _monthly_nickname_scenario_st(draw):
    """生成 AssistantMonthlyTask 的 nickname 测试场景。

    同一助教同一档位在同一月内有多条不同 nickname 的日度记录。
    验证聚合结果的 nickname == 时间最晚记录的 nickname。
    """
    assistant_id = draw(_assistant_id_st)
    level_code = draw(_level_code_st)
    month = date(2025, 1, 1)

    nicknames = draw(_distinct_nickname_st)
    month_start = month.replace(day=1)
    max_day = 31  # 2025-01 有 31 天

    # 为每个 nickname 生成一条记录，确保 stat_date 各不相同以便确定性排序
    daily_rows: List[Dict[str, Any]] = []
    used_days: Set[int] = set()
    for nick in nicknames:
        day = draw(
            st.integers(min_value=1, max_value=max_day).filter(
                lambda d, _used=frozenset(used_days): d not in _used
            )
        )
        used_days.add(day)
        row = {
            "site_id": 1,
            "assistant_id": assistant_id,
            "assistant_level_code": level_code,
            "assistant_level_name": _LEVEL_NAME_MAP[level_code],
            "assistant_nickname": nick,
            "stat_date": month_start.replace(day=day),
            # 业绩指标用固定值，本测试不关心
            **{f: 1 for f in _METRIC_FIELDS},
        }
        daily_rows.append(row)

    # 期望的 nickname = stat_date 最大的那条
    expected_nickname = max(daily_rows, key=lambda r: r["stat_date"])["assistant_nickname"]

    return {
        "daily_rows": daily_rows,
        "assistant_id": assistant_id,
        "level_code": level_code,
        "month": month,
        "expected_nickname": expected_nickname,
    }


@st.composite
def _finance_nickname_scenario_st(draw):
    """生成 AssistantFinanceTask 的 nickname 测试场景。

    同一助教同一天有多条不同 nickname 的服务记录（不同 start_use_time）。
    验证聚合结果的 nickname == start_use_time 最晚的那条。
    """
    from datetime import datetime

    assistant_id = draw(_assistant_id_st)
    stat_date = date(2025, 1, 15)
    nicknames = draw(_distinct_nickname_st)

    service_rows: List[Dict[str, Any]] = []
    used_hours: Set[int] = set()
    for nick in nicknames:
        hour = draw(
            st.integers(min_value=0, max_value=23).filter(
                lambda h, _used=frozenset(used_hours): h not in _used
            )
        )
        used_hours.add(hour)
        minute = draw(st.integers(min_value=0, max_value=59))
        service_rows.append({
            "site_id": 1,
            "assistant_id": assistant_id,
            "nickname": nick,
            "stat_date": stat_date,
            "start_use_time": datetime(2025, 1, 15, hour, minute, 0),
            "is_delete": 0,
        })

    expected_nickname = max(
        service_rows, key=lambda r: r["start_use_time"]
    )["nickname"]

    return {
        "service_rows": service_rows,
        "assistant_id": assistant_id,
        "stat_date": stat_date,
        "expected_nickname": expected_nickname,
    }


@st.composite
def _customer_nickname_scenario_st(draw):
    """生成 AssistantCustomerTask 的 nickname 测试场景。

    同一助教同一会员有多条不同 nickname 的服务记录（不同 service_date）。
    验证聚合结果的 nickname == service_date 最晚的那条。
    """
    assistant_id = draw(_assistant_id_st)
    member_id = draw(st.integers(min_value=1, max_value=9999))
    nicknames = draw(_distinct_nickname_st)

    service_rows: List[Dict[str, Any]] = []
    used_days: Set[int] = set()
    for nick in nicknames:
        day = draw(
            st.integers(min_value=1, max_value=28).filter(
                lambda d, _used=frozenset(used_days): d not in _used
            )
        )
        used_days.add(day)
        service_rows.append({
            "site_id": 1,
            "assistant_id": assistant_id,
            "member_id": member_id,
            "assistant_nickname": nick,
            "service_date": date(2025, 1, day),
            "is_delete": 0,
        })

    expected_nickname = max(
        service_rows, key=lambda r: r["service_date"]
    )["assistant_nickname"]

    return {
        "service_rows": service_rows,
        "assistant_id": assistant_id,
        "member_id": member_id,
        "expected_nickname": expected_nickname,
    }


# ── Property 4 测试类 ────────────────────────────────────────


class TestProperty4NicknameDescOrder:
    """Property 4: nickname 按时间倒序取值。

    对于任意助教在聚合周期内有多条不同 nickname 的记录，
    聚合结果中的 nickname 应等于时间最晚的那条记录的 nickname。
    """

    # ── 4a: AssistantMonthlyTask — 按 stat_date DESC 取 nickname ──

    @given(scenario=_monthly_nickname_scenario_st())
    @settings(max_examples=200)
    def test_monthly_nickname_equals_latest_stat_date(self, scenario):
        """AssistantMonthlyTask: nickname 应等于 stat_date 最晚记录的 nickname。"""
        results = simulate_extract_daily_aggregates(
            scenario["daily_rows"], site_id=1, month=scenario["month"],
        )
        # 定位到当前助教 + 档位的聚合行
        target = [
            r for r in results
            if r["assistant_id"] == scenario["assistant_id"]
            and r["assistant_level_code"] == scenario["level_code"]
        ]
        assert len(target) == 1, f"期望 1 行聚合结果，实际 {len(target)} 行"
        assert target[0]["assistant_nickname"] == scenario["expected_nickname"], (
            f"MonthlyTask nickname 应为 '{scenario['expected_nickname']}'，"
            f"实际为 '{target[0]['assistant_nickname']}'"
        )

    # ── 4b: AssistantFinanceTask — 按 start_use_time DESC 取 nickname ──

    @given(scenario=_finance_nickname_scenario_st())
    @settings(max_examples=200)
    def test_finance_nickname_equals_latest_start_use_time(self, scenario):
        """AssistantFinanceTask: nickname 应等于 start_use_time 最晚记录的 nickname。"""
        results = simulate_finance_daily_revenue(
            scenario["service_rows"],
            site_id=1,
            start_date=scenario["stat_date"],
            end_date=scenario["stat_date"],
        )
        target = [
            r for r in results
            if r["assistant_id"] == scenario["assistant_id"]
            and r["stat_date"] == scenario["stat_date"]
        ]
        assert len(target) == 1, f"期望 1 行聚合结果，实际 {len(target)} 行"
        assert target[0]["assistant_nickname"] == scenario["expected_nickname"], (
            f"FinanceTask nickname 应为 '{scenario['expected_nickname']}'，"
            f"实际为 '{target[0]['assistant_nickname']}'"
        )

    # ── 4c: AssistantCustomerTask — 按 service_date DESC 取 nickname ──

    @given(scenario=_customer_nickname_scenario_st())
    @settings(max_examples=200)
    def test_customer_nickname_equals_latest_service_date(self, scenario):
        """AssistantCustomerTask: nickname 应等于 service_date 最晚记录的 nickname。"""
        results = simulate_customer_service_pairs(
            scenario["service_rows"], site_id=1,
        )
        target = [
            r for r in results
            if r["assistant_id"] == scenario["assistant_id"]
            and r["member_id"] == scenario["member_id"]
        ]
        assert len(target) == 1, f"期望 1 行聚合结果，实际 {len(target)} 行"
        assert target[0]["assistant_nickname"] == scenario["expected_nickname"], (
            f"CustomerTask nickname 应为 '{scenario['expected_nickname']}'，"
            f"实际为 '{target[0]['assistant_nickname']}'"
        )


# ── Property 5: 工资按档位分段计算 ─────────────────────────────
# Feature: etl-aggregation-fix, Property 5: 工资按档位分段计算
# **Validates: Requirements 2.4**
#
# 对于任意助教在同一月有多个档位的月度汇总记录，
# AssistantSalaryTask 应为每个档位分别计算工资，
# 每个档位使用对应的 level_price 和 tier 配置，
# 且所有档位的工资记录数等于月度汇总的行数。


# ── 模拟 AssistantSalaryTask.transform() 的工资计算逻辑 ──
#
# 实际流程：
#   1. extract() 从 dws_assistant_monthly_summary 取出多行（同一助教不同档位）
#   2. transform() 遍历每行，调用 _calculate_salary()
#   3. _calculate_salary() 按行的 assistant_level_code 获取 level_price 和 tier
#   4. 每行独立计算工资，生成一条工资记录
#
# 本测试不模拟完整工资公式，只验证"分段"行为：
#   - 工资记录数 == 月度汇总行数
#   - 每条工资记录使用的是对应档位的 level_price


# 等级定价配置（模拟 cfg_assistant_level_price）
_LEVEL_PRICE_CONFIG = {
    "A": {"base_course_price": Decimal("98"), "bonus_course_price": Decimal("190")},
    "B": {"base_course_price": Decimal("108"), "bonus_course_price": Decimal("190")},
    "C": {"base_course_price": Decimal("118"), "bonus_course_price": Decimal("190")},
    "D": {"base_course_price": Decimal("128"), "bonus_course_price": Decimal("190")},
    "E": {"base_course_price": Decimal("138"), "bonus_course_price": Decimal("190")},
}

# 档位配置（模拟 cfg_performance_tier）
_TIER_CONFIG = {
    1: {"tier_code": "T1", "tier_name": "1档", "base_deduction": Decimal("18"), "bonus_deduction_ratio": Decimal("0.40")},
    2: {"tier_code": "T2", "tier_name": "2档", "base_deduction": Decimal("15"), "bonus_deduction_ratio": Decimal("0.38")},
    3: {"tier_code": "T3", "tier_name": "3档", "base_deduction": Decimal("13"), "bonus_deduction_ratio": Decimal("0.35")},
}


def _simulate_get_level_price(level_code: str) -> Dict[str, Any]:
    """模拟 get_level_price()：按档位代码返回等级定价。"""
    return _LEVEL_PRICE_CONFIG.get(level_code, _LEVEL_PRICE_CONFIG["A"])


def _simulate_get_tier(tier_id: int) -> Dict[str, Any]:
    """模拟 get_performance_tier_by_id()：按 tier_id 返回档位配置。"""
    return _TIER_CONFIG.get(tier_id, _TIER_CONFIG[1])


def simulate_salary_transform(
    monthly_summary: List[Dict[str, Any]],
    site_id: int,
    salary_month: date,
) -> List[Dict[str, Any]]:
    """模拟 AssistantSalaryTask.transform() 的核心逻辑。

    遍历每条月度汇总记录，按档位独立计算工资。
    简化版：只计算课时收入部分，足以验证"分段"行为。
    """
    results = []
    for summary in monthly_summary:
        level_code = summary.get("assistant_level_code")
        tier_id = summary.get("tier_id", 1)

        # 按档位获取定价和档位配置
        level_price = _simulate_get_level_price(level_code)
        tier = _simulate_get_tier(tier_id)

        base_hours = Decimal(str(summary.get("base_hours", 0)))
        bonus_hours = Decimal(str(summary.get("bonus_hours", 0)))

        base_course_price = level_price["base_course_price"]
        bonus_course_price = level_price["bonus_course_price"]
        base_deduction = tier["base_deduction"]
        bonus_deduction_ratio = tier["bonus_deduction_ratio"]

        # 基础课收入 = 基础课小时数 × (客户支付价格 - 专业课抽成)
        base_income = base_hours * (base_course_price - base_deduction)
        # 附加课收入 = 附加课小时数 × 附加课价格 × (1 - 打赏课抽成比例)
        bonus_income = bonus_hours * bonus_course_price * (Decimal("1") - bonus_deduction_ratio)

        results.append({
            "site_id": site_id,
            "assistant_id": summary["assistant_id"],
            "salary_month": salary_month,
            "assistant_level_code": level_code,
            "base_course_price": base_course_price,
            "bonus_course_price": bonus_course_price,
            "base_deduction": base_deduction,
            "bonus_deduction_ratio": bonus_deduction_ratio,
            "tier_id": tier_id,
            "base_income": base_income,
            "bonus_income": bonus_income,
        })

    return results


# ── Property 5 生成策略 ──────────────────────────────────────

# 档位 tier_id
_tier_id_st = st.sampled_from([1, 2, 3])

# 小时数（Decimal 精度）
_hours_st = st.decimals(
    min_value=0,
    max_value=Decimal("300"),
    places=2,
    allow_nan=False,
    allow_infinity=False,
)


@st.composite
def _salary_multi_level_scenario_st(draw):
    """生成同一助教在同一月有多个档位的月度汇总记录。

    返回：
    - monthly_summary: 月度汇总行列表（每行一个档位）
    - assistant_id: 助教 ID
    - salary_month: 工资月份
    - level_codes: 档位代码列表
    """
    assistant_id = draw(_assistant_id_st)
    salary_month = date(2025, 1, 1)

    # 随机选取 2~4 个不同档位（至少 2 个才能验证"分段"）
    n_levels = draw(st.integers(min_value=2, max_value=4))
    level_codes = draw(
        st.lists(
            _level_code_st,
            min_size=n_levels,
            max_size=n_levels,
            unique=True,
        )
    )

    monthly_summary: List[Dict[str, Any]] = []
    for code in level_codes:
        tier_id = draw(_tier_id_st)
        monthly_summary.append({
            "assistant_id": assistant_id,
            "assistant_nickname": f"助教{assistant_id}",
            "stat_month": salary_month,
            "assistant_level_code": code,
            "assistant_level_name": _LEVEL_NAME_MAP[code],
            "hire_date": date(2024, 6, 1),
            "is_new_hire": False,
            "effective_hours": float(draw(_hours_st)),
            "base_hours": float(draw(_hours_st)),
            "bonus_hours": float(draw(_hours_st)),
            "room_hours": float(draw(_hours_st)),
            "tier_id": tier_id,
            "tier_code": f"T{tier_id}",
            "tier_name": f"{tier_id}档",
            "rank_with_ties": draw(st.integers(min_value=1, max_value=10)),
        })

    return {
        "monthly_summary": monthly_summary,
        "assistant_id": assistant_id,
        "salary_month": salary_month,
        "level_codes": level_codes,
    }


# ── Property 5 测试类 ────────────────────────────────────────


class TestProperty5SalaryPerLevelSegment:
    """Property 5: 工资按档位分段计算。

    对于任意助教在同一月有多个档位的月度汇总记录，
    AssistantSalaryTask 应为每个档位分别计算工资，
    每个档位使用对应的 level_price 和 tier 配置，
    且所有档位的工资记录数等于月度汇总的行数。
    """

    @given(scenario=_salary_multi_level_scenario_st())
    @settings(max_examples=200)
    def test_salary_record_count_equals_summary_rows(self, scenario):
        """工资记录数应等于月度汇总行数（每个档位一条）。"""
        results = simulate_salary_transform(
            scenario["monthly_summary"],
            site_id=1,
            salary_month=scenario["salary_month"],
        )

        assert len(results) == len(scenario["monthly_summary"]), (
            f"月度汇总 {len(scenario['monthly_summary'])} 行，"
            f"但工资记录 {len(results)} 条"
        )

    @given(scenario=_salary_multi_level_scenario_st())
    @settings(max_examples=200)
    def test_each_salary_uses_correct_level_price(self, scenario):
        """每条工资记录应使用对应档位的 level_price。"""
        results = simulate_salary_transform(
            scenario["monthly_summary"],
            site_id=1,
            salary_month=scenario["salary_month"],
        )

        for record in results:
            level_code = record["assistant_level_code"]
            expected_price = _LEVEL_PRICE_CONFIG[level_code]

            assert record["base_course_price"] == expected_price["base_course_price"], (
                f"档位 {level_code}: base_course_price 应为 {expected_price['base_course_price']}，"
                f"实际为 {record['base_course_price']}"
            )
            assert record["bonus_course_price"] == expected_price["bonus_course_price"], (
                f"档位 {level_code}: bonus_course_price 应为 {expected_price['bonus_course_price']}，"
                f"实际为 {record['bonus_course_price']}"
            )

    @given(scenario=_salary_multi_level_scenario_st())
    @settings(max_examples=200)
    def test_each_salary_uses_correct_tier_config(self, scenario):
        """每条工资记录应使用对应 tier_id 的档位配置。"""
        results = simulate_salary_transform(
            scenario["monthly_summary"],
            site_id=1,
            salary_month=scenario["salary_month"],
        )

        for i, record in enumerate(results):
            tier_id = scenario["monthly_summary"][i]["tier_id"]
            expected_tier = _TIER_CONFIG[tier_id]

            assert record["base_deduction"] == expected_tier["base_deduction"], (
                f"tier_id={tier_id}: base_deduction 应为 {expected_tier['base_deduction']}，"
                f"实际为 {record['base_deduction']}"
            )
            assert record["bonus_deduction_ratio"] == expected_tier["bonus_deduction_ratio"], (
                f"tier_id={tier_id}: bonus_deduction_ratio 应为 {expected_tier['bonus_deduction_ratio']}，"
                f"实际为 {record['bonus_deduction_ratio']}"
            )

    @given(scenario=_salary_multi_level_scenario_st())
    @settings(max_examples=200)
    def test_salary_level_codes_match_summary(self, scenario):
        """工资记录的档位集合应与月度汇总的档位集合一致。"""
        results = simulate_salary_transform(
            scenario["monthly_summary"],
            site_id=1,
            salary_month=scenario["salary_month"],
        )

        result_codes = {r["assistant_level_code"] for r in results}
        expected_codes = set(scenario["level_codes"])

        assert result_codes == expected_codes, (
            f"工资记录档位 {result_codes} != 月度汇总档位 {expected_codes}"
        )
