# -*- coding: utf-8 -*-
"""
Feature: dataflow-field-completion, Property 7: 计时器记录完整性

**Validates: Requirements 15.2**

对于任意 ETL 步骤序列，计时器输出应包含每个步骤的名称、开始时间、结束时间和耗时，
且耗时等于结束时间减去开始时间。

测试策略：
- 使用 hypothesis 生成随机步骤名称列表（1-10 个步骤）
- 每个步骤可选包含 0-5 个子步骤
- 验证属性：
  1. 每个步骤的 to_dict() 输出包含 name、start_time、end_time、elapsed_ms
  2. elapsed_ms ≈ (end_time - start_time) 的毫秒数（允许 ±50ms 误差）
  3. 所有步骤名称都出现在 timer.steps 中
  4. timer.to_dict() 的 steps 数量等于实际添加的步骤数
  5. 子步骤的 elapsed_ms 也满足上述一致性
"""
from __future__ import annotations

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

from hypothesis import given, settings, HealthCheck
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 utils.timer import EtlTimer


# ══════════════════════════════════════════════════════════════════
# Hypothesis 策略
# ══════════════════════════════════════════════════════════════════

# 步骤名称：ASCII 字母 + 数字 + 下划线，模拟真实 ETL 任务名
_step_name = st.text(
    alphabet=st.sampled_from("ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789"),
    min_size=3,
    max_size=20,
)

# 子步骤名称
_sub_step_name = st.text(
    alphabet=st.sampled_from("abcdefghijklmnopqrstuvwxyz_0123456789"),
    min_size=2,
    max_size=15,
)


@st.composite
def _step_spec(draw):
    """生成一个步骤规格：(步骤名, [子步骤名列表])"""
    name = draw(_step_name)
    # 0-5 个子步骤，名称唯一
    sub_names = draw(
        st.lists(_sub_step_name, min_size=0, max_size=5, unique=True)
    )
    return (name, sub_names)


# 生成 1-10 个步骤，步骤名唯一
_steps_strategy = st.lists(
    _step_spec(),
    min_size=1,
    max_size=10,
).filter(
    # 确保步骤名唯一
    lambda specs: len(set(s[0] for s in specs)) == len(specs)
)

# 允许的时间误差（毫秒）——perf_counter 与 datetime.now 之间存在微小差异
_TOLERANCE_MS = 50.0


# ══════════════════════════════════════════════════════════════════
# 辅助函数
# ══════════════════════════════════════════════════════════════════

def _run_timer(step_specs: list[tuple[str, list[str]]]) -> EtlTimer:
    """按给定步骤规格运行计时器，返回完成后的 timer 实例"""
    timer = EtlTimer()
    timer.start()

    for step_name, sub_names in step_specs:
        timer.start_step(step_name)
        for sub_name in sub_names:
            timer.start_sub_step(step_name, sub_name)
            # 极短暂停，确保 start/end 时间有差异
            time.sleep(0.001)
            timer.stop_sub_step(step_name, sub_name)
        time.sleep(0.001)
        timer.stop_step(step_name)

    timer.finish(write_report=False)
    return timer


def _parse_iso(iso_str: str) -> datetime:
    """解析 ISO 格式时间字符串"""
    return datetime.fromisoformat(iso_str)


# ══════════════════════════════════════════════════════════════════
# Property 7a: to_dict() 输出包含必要字段
# ══════════════════════════════════════════════════════════════════

@given(step_specs=_steps_strategy)
@settings(
    max_examples=100,
    deadline=None,
    suppress_health_check=[HealthCheck.function_scoped_fixture, HealthCheck.too_slow],
)
def test_step_dict_contains_required_fields(step_specs):
    """
    **Validates: Requirements 15.2**

    每个步骤的 to_dict() 输出必须包含 name、start_time、end_time、elapsed_ms 字段。
    """
    timer = _run_timer(step_specs)

    for step in timer.steps:
        d = step.to_dict()
        for key in ("name", "start_time", "end_time", "elapsed_ms"):
            assert key in d, f"步骤 {step.name} 的 to_dict() 缺少字段: {key}"
        # start_time 和 end_time 不为 None
        assert d["start_time"] is not None, f"步骤 {step.name} 的 start_time 为 None"
        assert d["end_time"] is not None, f"步骤 {step.name} 的 end_time 为 None"


# ══════════════════════════════════════════════════════════════════
# Property 7b: elapsed_ms ≈ (end_time - start_time)
# ══════════════════════════════════════════════════════════════════

@given(step_specs=_steps_strategy)
@settings(
    max_examples=100,
    deadline=None,
    suppress_health_check=[HealthCheck.function_scoped_fixture, HealthCheck.too_slow],
)
def test_step_elapsed_ms_consistent_with_timestamps(step_specs):
    """
    **Validates: Requirements 15.2**

    每个步骤的 elapsed_ms 应近似等于 (end_time - start_time) 的毫秒数。
    允许 ±50ms 误差（perf_counter 与 datetime.now 的微小差异）。
    """
    timer = _run_timer(step_specs)

    for step in timer.steps:
        d = step.to_dict()
        start_dt = _parse_iso(d["start_time"])
        end_dt = _parse_iso(d["end_time"])
        wall_ms = (end_dt - start_dt).total_seconds() * 1000
        elapsed_ms = d["elapsed_ms"]

        diff = abs(elapsed_ms - wall_ms)
        assert diff <= _TOLERANCE_MS, (
            f"步骤 {step.name}: elapsed_ms={elapsed_ms:.3f} 与 "
            f"wall_clock_ms={wall_ms:.3f} 差异 {diff:.3f}ms 超过容差 {_TOLERANCE_MS}ms"
        )


# ══════════════════════════════════════════════════════════════════
# Property 7c: 所有步骤名称都出现在 timer.steps 中
# ══════════════════════════════════════════════════════════════════

@given(step_specs=_steps_strategy)
@settings(
    max_examples=100,
    deadline=None,
    suppress_health_check=[HealthCheck.function_scoped_fixture, HealthCheck.too_slow],
)
def test_all_step_names_present(step_specs):
    """
    **Validates: Requirements 15.2**

    所有添加的步骤名称都必须出现在 timer.steps 列表中。
    """
    timer = _run_timer(step_specs)
    recorded_names = {s.name for s in timer.steps}

    for step_name, _ in step_specs:
        assert step_name in recorded_names, (
            f"步骤 {step_name!r} 未出现在 timer.steps 中"
        )


# ══════════════════════════════════════════════════════════════════
# Property 7d: timer.to_dict() 的 steps 数量等于实际步骤数
# ══════════════════════════════════════════════════════════════════

@given(step_specs=_steps_strategy)
@settings(
    max_examples=100,
    deadline=None,
    suppress_health_check=[HealthCheck.function_scoped_fixture, HealthCheck.too_slow],
)
def test_timer_dict_step_count_matches(step_specs):
    """
    **Validates: Requirements 15.2**

    timer.to_dict() 输出的 steps 数量应等于实际添加的步骤数。
    """
    timer = _run_timer(step_specs)
    timer_dict = timer.to_dict()

    assert len(timer_dict["steps"]) == len(step_specs), (
        f"to_dict() steps 数量 {len(timer_dict['steps'])} "
        f"!= 实际步骤数 {len(step_specs)}"
    )


# ══════════════════════════════════════════════════════════════════
# Property 7e: 子步骤的 elapsed_ms 也满足一致性
# ══════════════════════════════════════════════════════════════════

@given(step_specs=_steps_strategy)
@settings(
    max_examples=100,
    deadline=None,
    suppress_health_check=[HealthCheck.function_scoped_fixture, HealthCheck.too_slow],
)
def test_sub_step_elapsed_ms_consistent(step_specs):
    """
    **Validates: Requirements 15.2**

    子步骤的 elapsed_ms 也应近似等于 (end_time - start_time) 的毫秒数，
    且 to_dict() 输出包含必要字段。
    """
    timer = _run_timer(step_specs)

    for step in timer.steps:
        step_dict = step.to_dict()
        for child_dict in step_dict["children"]:
            # 必要字段检查
            for key in ("name", "start_time", "end_time", "elapsed_ms"):
                assert key in child_dict, (
                    f"子步骤 {child_dict.get('name', '?')} 的 to_dict() 缺少字段: {key}"
                )

            if child_dict["end_time"] is None:
                continue

            start_dt = _parse_iso(child_dict["start_time"])
            end_dt = _parse_iso(child_dict["end_time"])
            wall_ms = (end_dt - start_dt).total_seconds() * 1000
            elapsed_ms = child_dict["elapsed_ms"]

            diff = abs(elapsed_ms - wall_ms)
            assert diff <= _TOLERANCE_MS, (
                f"子步骤 {child_dict['name']}: elapsed_ms={elapsed_ms:.3f} 与 "
                f"wall_clock_ms={wall_ms:.3f} 差异 {diff:.3f}ms 超过容差 {_TOLERANCE_MS}ms"
            )
