# -*- coding: utf-8 -*-
"""
dws_helpers 函数等价性 — 属性测试

Feature: etl-dws-flow-refactor, Property 3: dws_helpers 函数等价性
测试位置：apps/etl/connectors/feiqiu/tests/unit/

使用 hypothesis 验证 dws_helpers 模块中各辅助函数的正确性属性。
**Validates: Requirements 2.3**
"""
from __future__ import annotations

import sys
from datetime import date, datetime, timedelta
from decimal import Decimal
from pathlib import Path

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

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

from tasks.dws.dws_helpers import (
    mask_mobile,
    calc_days_since,
    parse_id_list,
    safe_division,
)


# ============================================================================
# 策略（Strategies）
# ============================================================================

# 11 位数字字符串（标准中国手机号长度）
st_mobile_11 = st.from_regex(r"[0-9]{11}", fullmatch=True)

# 7 位及以上的字符串（mask_mobile 会执行脱敏）
st_mobile_long = st.from_regex(r"[0-9]{7,15}", fullmatch=True)

# 短于 7 位的字符串（mask_mobile 原样返回）
st_mobile_short = st.text(
    alphabet=st.characters(whitelist_categories=("Nd",)),
    min_size=0,
    max_size=6,
)

# 日期策略
st_date = st.dates(min_value=date(2000, 1, 1), max_value=date(2035, 12, 31))

# datetime 策略（用于测试 calc_days_since 的 datetime→date 自动转换）
st_datetime = st.datetimes(
    min_value=datetime(2000, 1, 1),
    max_value=datetime(2035, 12, 31),
)

# 逗号分隔的正整数字符串
st_comma_int_str = st.lists(
    st.integers(min_value=0, max_value=999999),
    min_size=0,
    max_size=20,
).map(lambda nums: ",".join(str(n) for n in nums))

# 安全除法的数值策略
st_numerator = st.decimals(
    min_value=Decimal("-1000000"),
    max_value=Decimal("1000000"),
    allow_nan=False,
    allow_infinity=False,
)
st_denominator_nonzero = st.decimals(
    min_value=Decimal("0.001"),
    max_value=Decimal("1000000"),
    allow_nan=False,
    allow_infinity=False,
).flatmap(
    lambda d: st.sampled_from([d, -d])
)


# ============================================================================
# Property 3.1: mask_mobile — 11 位字符串脱敏
# Feature: etl-dws-flow-refactor, Property 3: dws_helpers 函数等价性
# ============================================================================

class TestMaskMobileProperty:
    """mask_mobile 属性测试"""

    @given(mobile=st_mobile_11)
    @settings(max_examples=100)
    def test_11_digit_mask_structure(self, mobile: str):
        """对任意 11 位数字字符串，mask_mobile 应返回前 3 位 + **** + 后 4 位"""
        # **Validates: Requirements 2.3**
        result = mask_mobile(mobile)
        assert result is not None
        assert len(result) == 11  # 3 + 4 + 4
        assert result[:3] == mobile[:3]
        assert result[3:7] == "****"
        assert result[7:] == mobile[7:]

    @given(mobile=st_mobile_long)
    @settings(max_examples=100)
    def test_long_mobile_preserves_head_tail(self, mobile: str):
        """对任意 ≥7 位字符串，mask_mobile 保留前 3 位和后 4 位"""
        # **Validates: Requirements 2.3**
        result = mask_mobile(mobile)
        assert result is not None
        assert result[:3] == mobile[:3]
        assert result[-4:] == mobile[-4:]
        assert "****" in result

    @given(mobile=st_mobile_short)
    @settings(max_examples=100)
    def test_short_mobile_returned_as_is(self, mobile: str):
        """短于 7 位的号码原样返回"""
        # **Validates: Requirements 2.3**
        result = mask_mobile(mobile)
        assert result == mobile

    def test_none_returns_none(self):
        """None 输入返回 None"""
        assert mask_mobile(None) is None

    def test_empty_returns_empty(self):
        """空字符串原样返回（len < 7）"""
        assert mask_mobile("") == ""


# ============================================================================
# Property 3.2: calc_days_since — 日期差计算
# Feature: etl-dws-flow-refactor, Property 3: dws_helpers 函数等价性
# ============================================================================

class TestCalcDaysSinceProperty:
    """calc_days_since 属性测试"""

    @given(stat_date=st_date, last_date=st_date)
    @settings(max_examples=100)
    def test_date_diff_equals_subtraction(self, stat_date: date, last_date: date):
        """对任意两个 date，calc_days_since(stat_date, last_date) == (stat_date - last_date).days"""
        # **Validates: Requirements 2.3**
        result = calc_days_since(stat_date, last_date)
        expected = (stat_date - last_date).days
        assert result == expected

    @given(stat_date=st_date, last_dt=st_datetime)
    @settings(max_examples=100)
    def test_datetime_auto_converts_to_date(self, stat_date: date, last_dt: datetime):
        """当 last_date 为 datetime 时，自动取 .date() 后计算"""
        # **Validates: Requirements 2.3**
        result = calc_days_since(stat_date, last_dt)
        expected = (stat_date - last_dt.date()).days
        assert result == expected

    @given(stat_date=st_date)
    @settings(max_examples=100)
    def test_none_last_date_returns_none(self, stat_date: date):
        """last_date 为 None 时返回 None"""
        # **Validates: Requirements 2.3**
        result = calc_days_since(stat_date, None)
        assert result is None


# ============================================================================
# Property 3.3: parse_id_list — 逗号分隔整数解析
# Feature: etl-dws-flow-refactor, Property 3: dws_helpers 函数等价性
# ============================================================================

class TestParseIdListProperty:
    """parse_id_list 属性测试"""

    @given(nums=st.lists(st.integers(min_value=0, max_value=999999), min_size=1, max_size=20))
    @settings(max_examples=100)
    def test_comma_string_roundtrip(self, nums: list[int]):
        """对任意非负整数列表，逗号拼接后 parse_id_list 应返回对应的 int 集合"""
        # **Validates: Requirements 2.3**
        csv_str = ",".join(str(n) for n in nums)
        result = parse_id_list(csv_str)
        assert result == set(nums)

    @given(nums=st.lists(st.integers(min_value=0, max_value=999999), min_size=0, max_size=20))
    @settings(max_examples=100)
    def test_list_input_returns_same_set(self, nums: list[int]):
        """list 输入也应返回对应的 int 集合"""
        # **Validates: Requirements 2.3**
        result = parse_id_list(nums)
        assert result == set(nums)

    def test_empty_string_returns_empty_set(self):
        """空字符串返回空集合"""
        assert parse_id_list("") == set()

    def test_none_returns_empty_set(self):
        """None 返回空集合"""
        assert parse_id_list(None) == set()

    def test_non_digit_items_skipped(self):
        """非数字项被静默跳过"""
        assert parse_id_list("1,abc,3") == {1, 3}


# ============================================================================
# Property 3.4: safe_division — 安全除法
# Feature: etl-dws-flow-refactor, Property 3: dws_helpers 函数等价性
# ============================================================================

class TestSafeDivisionProperty:
    """safe_division 属性测试"""

    @given(numerator=st_numerator, denominator=st_denominator_nonzero)
    @settings(max_examples=100)
    def test_nonzero_denominator_returns_quotient(self, numerator: Decimal, denominator: Decimal):
        """分母非零时，safe_division 返回 Decimal(numerator) / Decimal(denominator)"""
        # **Validates: Requirements 2.3**
        result = safe_division(numerator, denominator)
        expected = Decimal(str(numerator)) / Decimal(str(denominator))
        assert result == expected

    @given(numerator=st_numerator)
    @settings(max_examples=100)
    def test_zero_denominator_returns_default(self, numerator: Decimal):
        """分母为零时返回默认值"""
        # **Validates: Requirements 2.3**
        default = Decimal("99.99")
        result = safe_division(numerator, 0, default=default)
        assert result == default

    def test_zero_denominator_default_is_zero(self):
        """分母为零且未指定 default 时返回 Decimal('0')"""
        assert safe_division(100, 0) == Decimal("0")

    def test_none_denominator_returns_default(self):
        """分母为 None 时返回默认值"""
        assert safe_division(100, None) == Decimal("0")

    def test_result_is_decimal(self):
        """返回值始终为 Decimal 类型"""
        result = safe_division(10, 3)
        assert isinstance(result, Decimal)
