# -*- coding: utf-8 -*-
"""DwdLoadTask 返回值格式属性测试 — 使用 hypothesis 验证返回值的通用正确性属性。

纯单元测试，使用 MagicMock 构造 DwdLoadTask 实例，不涉及真实数据库连接。

# Feature: etl-aggregation-fix, Property 1: DwdLoadTask 返回值格式一致性
# **Validates: Requirements 1.1**
"""
from __future__ import annotations

import logging
from datetime import datetime, timezone
from types import SimpleNamespace
from unittest.mock import MagicMock, patch

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

from tasks.dwd.dwd_load_task import DwdLoadTask


# ── 辅助：构造最小可用 DwdLoadTask 实例 ─────────────────────

def _make_task() -> DwdLoadTask:
    config = MagicMock()
    config.get = MagicMock(side_effect=lambda key, default=None: default)

    fake_conn = MagicMock()
    fake_cursor = MagicMock()
    fake_cursor.__enter__ = MagicMock(return_value=fake_cursor)
    fake_cursor.__exit__ = MagicMock(return_value=False)
    fake_conn.cursor.return_value = fake_cursor

    db = MagicMock()
    db.conn = fake_conn
    api = MagicMock()
    logger = logging.getLogger("test_return_format_pbt")
    return DwdLoadTask(config, db, api, logger)


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

# 生成 TABLE_MAP：1~8 张表，每张表随机决定是否失败
_table_name_st = st.text(
    alphabet=st.sampled_from("abcdefghijklmnopqrstuvwxyz_"),
    min_size=3, max_size=12,
)

# 生成一组 (dwd_table, ods_table, should_fail) 三元组
_table_entry_st = st.tuples(
    _table_name_st,  # dwd 表名后缀
    _table_name_st,  # ods 表名后缀
    st.booleans(),   # 是否模拟失败
)

_table_map_st = st.lists(_table_entry_st, min_size=0, max_size=8, unique_by=lambda t: t[0])


# ── Property 1: DwdLoadTask 返回值格式一致性 ─────────────────
# Feature: etl-aggregation-fix, Property 1: DwdLoadTask 返回值格式一致性
# **Validates: Requirements 1.1**
#
# 对于任意 DwdLoadTask.load() 的执行结果，返回字典中：
# - `errors` 键的值应为 int 类型
# - `error_details` 键的值应为 list 类型
# - `errors` == len(`error_details`)


class TestProperty1DwdLoadReturnFormat:
    """Property 1: DwdLoadTask 返回值格式一致性。"""

    @given(table_entries=_table_map_st)
    @settings(max_examples=200)
    def test_errors_is_int_and_equals_len_error_details(self, table_entries):
        """对于任意表映射和失败模式，errors 始终为 int 且等于 len(error_details)。"""
        task = _make_task()

        # 构造 TABLE_MAP 和失败集合
        table_map = {}
        fail_set = set()
        for dwd_suffix, ods_suffix, should_fail in table_entries:
            dwd_name = f"dwd.dim_{dwd_suffix}"
            ods_name = f"ods.{ods_suffix}"
            table_map[dwd_name] = ods_name
            if should_fail:
                fail_set.add(dwd_name)

        # _get_columns：失败表抛异常，成功表返回空列（跳过，不计入 summary 也不计入 errors）
        def fake_get_columns(cur, table):
            if table in fail_set:
                raise RuntimeError(f"模拟失败: {table}")
            return []  # 空列 → 跳过

        task._get_columns = fake_get_columns

        with patch.object(DwdLoadTask, "TABLE_MAP", table_map):
            ctx = SimpleNamespace(
                window_start=datetime(2025, 1, 1, tzinfo=timezone.utc),
                window_end=datetime(2025, 1, 2, tzinfo=timezone.utc),
            )
            result = task.load({"now": datetime.now(timezone.utc)}, ctx)

        # 核心断言：返回值格式一致性
        assert isinstance(result["errors"], int), (
            f"errors 应为 int，实际为 {type(result['errors']).__name__}"
        )
        assert isinstance(result["error_details"], list), (
            f"error_details 应为 list，实际为 {type(result['error_details']).__name__}"
        )
        assert result["errors"] == len(result["error_details"]), (
            f"errors={result['errors']} != len(error_details)={len(result['error_details'])}"
        )
        # errors 应等于预期失败数
        assert result["errors"] == len(fail_set), (
            f"errors={result['errors']} != 预期失败数={len(fail_set)}"
        )
        assert "tables" in result


# ── Property 2: _accumulate_counts 类型安全累加 ─────────────
# Feature: etl-aggregation-fix, Property 2: _accumulate_counts 类型安全累加
# **Validates: Requirements 1.2**
#
# 对于任意包含 int、float、list 类型值的计数字典，
# _accumulate_counts() 应将 int/float 直接累加，将 list 转为 len() 后累加，
# 且不抛出异常。

from tasks.base_task import BaseTask

# ── 生成策略：计数字典值 ──────────────────────────────────────

# 合法计数值：int / float / list（任意元素）
_count_value_st = st.one_of(
    st.integers(min_value=0, max_value=10_000),
    st.floats(min_value=0.0, max_value=10_000.0, allow_nan=False, allow_infinity=False),
    st.lists(st.integers(), min_size=0, max_size=50),
)

# 计数字典：1~10 个键，值为 int/float/list
_count_key_st = st.text(
    alphabet=st.sampled_from("abcdefghijklmnopqrstuvwxyz_"),
    min_size=2, max_size=10,
)

_count_dict_st = st.dictionaries(
    keys=_count_key_st,
    values=_count_value_st,
    min_size=0,
    max_size=10,
)


def _numeric_value(v) -> int | float:
    """将 int/float/list 统一转为数值（list → len）。"""
    if isinstance(v, (int, float)):
        return v
    if isinstance(v, list):
        return len(v)
    return 0


class TestProperty2AccumulateCountsTypeSafe:
    """Property 2: _accumulate_counts 类型安全累加。"""

    @given(dicts=st.lists(_count_dict_st, min_size=1, max_size=5))
    @settings(max_examples=200)
    def test_accumulate_never_raises_and_sums_correctly(self, dicts):
        """对于任意 int/float/list 值的计数字典序列，累加不抛异常且数值正确。"""
        total: dict = {}
        for d in dicts:
            BaseTask._accumulate_counts(total, d)

        # 手动计算期望值
        expected: dict = {}
        for d in dicts:
            for k, v in d.items():
                nv = _numeric_value(v)
                if isinstance(nv, (int, float)):
                    expected[k] = (expected.get(k) or 0) + nv
                else:
                    expected.setdefault(k, v)

        # 断言：所有可累加键的值应与手动计算一致
        for k, ev in expected.items():
            assert k in total, f"键 '{k}' 应存在于累加结果中"
            if isinstance(ev, (int, float)):
                assert abs(total[k] - ev) < 1e-9, (
                    f"键 '{k}': 累加结果={total[k]}, 期望={ev}"
                )

    @given(current=_count_dict_st)
    @settings(max_examples=200)
    def test_single_dict_values_match_numeric_conversion(self, current):
        """单次累加：int/float 保持原值，list 转为 len()。"""
        total: dict = {}
        BaseTask._accumulate_counts(total, current)

        for k, v in current.items():
            if isinstance(v, (int, float)):
                assert abs(total[k] - v) < 1e-9, (
                    f"键 '{k}': int/float 应直接累加，结果={total[k]}, 原值={v}"
                )
            elif isinstance(v, list):
                assert total[k] == len(v), (
                    f"键 '{k}': list 应转为 len()={len(v)}, 实际={total[k]}"
                )

    @given(current=_count_dict_st)
    @settings(max_examples=100)
    def test_none_total_key_initializes_correctly(self, current):
        """当 total 中键不存在时（get 返回 None），or 0 应正确初始化。"""
        total: dict = {}
        result = BaseTask._accumulate_counts(total, current)
        # 返回值应与 total 是同一对象
        assert result is total
        # 所有数值键应 >= 0
        for k, v in result.items():
            if isinstance(v, (int, float)):
                assert v >= 0, f"键 '{k}' 的值不应为负: {v}"
