# -*- coding: utf-8 -*-
"""
Feature: spi-spending-power-index — SPI 消费力指数属性测试

使用 hypothesis 验证 SPI 算法的正确性属性：
- Property 1: SPI 总分非负性
- Property 2: Level 子分关于消费金额单调非递减
- Property 3: Speed 子分关于 spend_30 单调非递减
- Property 4: Stability 子分取值范围 [0, 1]
- Property 5: Display Score 取值范围 [0, 10]

测试策略：
- 子分计算为 @staticmethod 纯函数，不依赖数据库，直接调用
- batch_normalize_to_display 为实例方法，通过 MagicMock 构造最小实例
"""
from __future__ import annotations

import sys
from pathlib import Path
from unittest.mock import MagicMock

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

# ── 将 ETL 模块加入 sys.path ──
_ETL_ROOT = (
    Path(__file__).resolve().parent.parent
    / "apps" / "etl" / "connectors" / "feiqiu"
)
if str(_ETL_ROOT) not in sys.path:
    sys.path.insert(0, str(_ETL_ROOT))

from tasks.dws.index.spending_power_index_task import (
    SpendingPowerIndexTask,
    SPIMemberFeatures,
)
from tasks.dws.index.base_index_task import BaseIndexTask


# ══════════════════════════════════════════════════════════════════
# 辅助：构造最小可用的 SpendingPowerIndexTask 实例（仅用于 Property 5）
# ══════════════════════════════════════════════════════════════════

def _make_spi_task() -> SpendingPowerIndexTask:
    """构造不依赖真实 DB/API 的 SPI 任务实例，仅用于调用 batch_normalize_to_display。"""
    config = MagicMock()
    # BaseTask.__init__ 会调用 config.get("app.timezone", "Asia/Shanghai")
    # MagicMock.get() 默认返回 Mock 对象，导致 ZoneInfo 报错，需要正确返回字符串
    config.get = lambda key, default=None: {
        "app.timezone": "Asia/Shanghai",
    }.get(key, default)
    db = MagicMock()
    api = MagicMock()
    logger = MagicMock()
    return SpendingPowerIndexTask(config, db, api, logger)


# ══════════════════════════════════════════════════════════════════
# Property 1: SPI 总分非负性
# ══════════════════════════════════════════════════════════════════

@given(
    level=st.floats(min_value=0, max_value=100),
    speed=st.floats(min_value=0, max_value=100),
    stability=st.floats(min_value=0, max_value=1),
)
@settings(max_examples=200)
def test_spi_raw_non_negative(level, speed, stability):
    """Property 1: SPI 总分非负性

    对于任意非负的 Level、Speed、Stability 子分，
    compute_spi_raw 的返回值应为非负。

    **Validates: Requirements 6.1, 10.1**
    """
    params = SpendingPowerIndexTask.DEFAULT_PARAMS
    result = SpendingPowerIndexTask.compute_spi_raw(level, speed, stability, params)
    assert result >= 0, f"SPI_raw={result} < 0 (L={level}, S={speed}, P={stability})"


# ══════════════════════════════════════════════════════════════════
# Property 2: Level 子分关于消费金额单调非递减
# ══════════════════════════════════════════════════════════════════

@given(
    spend_30=st.floats(min_value=0, max_value=50000),
    spend_90=st.floats(min_value=0, max_value=150000),
    recharge_90=st.floats(min_value=0, max_value=100000),
    avg_ticket_90=st.floats(min_value=0, max_value=5000),
    delta=st.floats(min_value=0.01, max_value=10000),
)
@settings(max_examples=200)
def test_level_monotonic_on_spend(spend_30, spend_90, recharge_90, avg_ticket_90, delta):
    """Property 2: Level 子分关于消费金额单调非递减

    在其他条件不变时，增加 spend_30 或 spend_90 不会导致 Level 子分下降。

    **Validates: Requirements 3.1, 10.2**
    """
    params = SpendingPowerIndexTask.DEFAULT_PARAMS

    base = SPIMemberFeatures(
        member_id=1, site_id=1,
        spend_30=spend_30, spend_90=spend_90,
        recharge_90=recharge_90, avg_ticket_90=avg_ticket_90,
    )
    level_before = SpendingPowerIndexTask.compute_level(base, params)

    # 增加 spend_30
    inc_30 = SPIMemberFeatures(
        member_id=1, site_id=1,
        spend_30=spend_30 + delta, spend_90=spend_90,
        recharge_90=recharge_90, avg_ticket_90=avg_ticket_90,
    )
    level_after_30 = SpendingPowerIndexTask.compute_level(inc_30, params)
    assert level_after_30 >= level_before, (
        f"Level 下降: spend_30 增加 {delta} 后 {level_after_30} < {level_before}"
    )

    # 增加 spend_90
    inc_90 = SPIMemberFeatures(
        member_id=1, site_id=1,
        spend_30=spend_30, spend_90=spend_90 + delta,
        recharge_90=recharge_90, avg_ticket_90=avg_ticket_90,
    )
    level_after_90 = SpendingPowerIndexTask.compute_level(inc_90, params)
    assert level_after_90 >= level_before, (
        f"Level 下降: spend_90 增加 {delta} 后 {level_after_90} < {level_before}"
    )


# ══════════════════════════════════════════════════════════════════
# Property 3: Speed 子分关于 spend_30 单调非递减
# ══════════════════════════════════════════════════════════════════

@given(
    spend_30=st.floats(min_value=0, max_value=50000),
    spend_90=st.floats(min_value=0, max_value=150000),
    visit_days_30=st.integers(min_value=0, max_value=30),
    daily_spend_ewma_90=st.floats(min_value=0, max_value=10000),
    delta=st.floats(min_value=0.01, max_value=10000),
)
@settings(max_examples=200)
def test_speed_monotonic_on_spend_30(spend_30, spend_90, visit_days_30, daily_spend_ewma_90, delta):
    """Property 3: Speed 子分关于 spend_30 单调非递减

    在其他条件不变时，增加 spend_30 不会导致 Speed 子分下降。

    **Validates: Requirements 4.1, 4.4, 10.3**
    """
    params = SpendingPowerIndexTask.DEFAULT_PARAMS

    base = SPIMemberFeatures(
        member_id=1, site_id=1,
        spend_30=spend_30, spend_90=spend_90,
        visit_days_30=visit_days_30,
        daily_spend_ewma_90=daily_spend_ewma_90,
    )
    speed_before = SpendingPowerIndexTask.compute_speed(base, params)

    inc = SPIMemberFeatures(
        member_id=1, site_id=1,
        spend_30=spend_30 + delta, spend_90=spend_90,
        visit_days_30=visit_days_30,
        daily_spend_ewma_90=daily_spend_ewma_90,
    )
    speed_after = SpendingPowerIndexTask.compute_speed(inc, params)
    assert speed_after >= speed_before, (
        f"Speed 下降: spend_30 增加 {delta} 后 {speed_after} < {speed_before}"
    )


# ══════════════════════════════════════════════════════════════════
# Property 4: Stability 子分取值范围 [0, 1]
# ══════════════════════════════════════════════════════════════════

@given(active_weeks=st.integers(min_value=0, max_value=13))
@settings(max_examples=200)
def test_stability_in_range(active_weeks):
    """Property 4: Stability 子分取值范围 [0, 1]

    对于任意 active_weeks_90 ∈ [0, 13]，compute_stability 返回值应在 [0, 1]。

    **Validates: Requirements 5.2, 5.4, 10.4**
    """
    params = SpendingPowerIndexTask.DEFAULT_PARAMS

    features = SPIMemberFeatures(
        member_id=1, site_id=1,
        active_weeks_90=active_weeks,
    )
    stability = SpendingPowerIndexTask.compute_stability(features, params)
    assert 0 <= stability <= 1, (
        f"Stability={stability} 超出 [0, 1] (active_weeks_90={active_weeks})"
    )


# ══════════════════════════════════════════════════════════════════
# Property 5: Display Score 取值范围 [0, 10]
# ══════════════════════════════════════════════════════════════════

@given(
    raw_scores=st.lists(
        st.floats(min_value=0, max_value=1000),
        min_size=1,
        max_size=50,
    ),
)
@settings(max_examples=200)
def test_display_score_in_range(raw_scores):
    """Property 5: Display Score 取值范围 [0, 10]

    对于任意非空的非负 raw_score 列表，batch_normalize_to_display
    映射后的 display_score 应在 [0.00, 10.00]。

    **Validates: Requirements 6.6, 10.5**
    """
    task = _make_spi_task()

    # 构造 (entity_id, raw_score) 输入
    input_scores = [(i, s) for i, s in enumerate(raw_scores)]

    results = task.batch_normalize_to_display(
        raw_scores=input_scores,
        compression=None,       # 无压缩，直接 MinMax
        use_smoothing=False,    # 不使用 EWMA 平滑（避免 DB 调用）
    )

    for entity_id, raw_score, display_score in results:
        assert 0.0 <= display_score <= 10.0, (
            f"display_score={display_score} 超出 [0, 10] (raw={raw_score})"
        )
