# -*- coding: utf-8 -*-
"""TaskConfig 属性测试（Property-Based Testing）。

使用 hypothesis 验证 TaskConfig 相关的通用正确性属性：
- Property 1: TaskConfig 序列化往返一致性
- Property 6: 时间窗口验证
- Property 7: TaskConfig 到 CLI 命令转换完整性
"""

import datetime

from hypothesis import given, settings, assume
from hypothesis import strategies as st
from pydantic import ValidationError

from app.schemas.tasks import TaskConfigSchema
from app.services.cli_builder import CLIBuilder, VALID_FLOWS, VALID_PROCESSING_MODES
from app.services.task_registry import ALL_TASKS

# ---------------------------------------------------------------------------
# 策略（Strategies）
# ---------------------------------------------------------------------------

# 从真实任务注册表中采样任务代码
_task_codes = [t.code for t in ALL_TASKS]

_tasks_st = st.lists(
    st.sampled_from(_task_codes),
    min_size=1,
    max_size=5,
    unique=True,
)

_flow_st = st.sampled_from(sorted(VALID_FLOWS))
_processing_mode_st = st.sampled_from(sorted(VALID_PROCESSING_MODES))
_window_mode_st = st.sampled_from(["lookback", "custom"])

# 日期策略：生成 YYYY-MM-DD 格式字符串
_date_st = st.dates(
    min_value=datetime.date(2020, 1, 1),
    max_value=datetime.date(2030, 12, 31),
).map(lambda d: d.isoformat())

_window_split_st = st.sampled_from([None, "none", "day"])
_window_split_days_st = st.one_of(st.none(), st.sampled_from([1, 10, 30]))
_lookback_hours_st = st.integers(min_value=1, max_value=720)
_overlap_seconds_st = st.integers(min_value=0, max_value=7200)
_store_id_st = st.one_of(st.none(), st.integers(min_value=1, max_value=2**31 - 1))

# DWD 表名采样
_dwd_table_names = [
    "dwd.dim_site",
    "dwd.dim_member",
    "dwd.dwd_settlement_head",
]
_dwd_only_tables_st = st.one_of(
    st.none(),
    st.lists(st.sampled_from(_dwd_table_names), min_size=1, max_size=3, unique=True),
)


def _valid_task_config_st():
    """生成有效的 TaskConfigSchema 的复合策略。

    确保 window_mode=custom 时 window_end >= window_start，
    避免触发 Pydantic 验证错误。
    """

    @st.composite
    def _build(draw):
        tasks = draw(_tasks_st)
        flow_id = draw(_flow_st)
        processing_mode = draw(_processing_mode_st)
        dry_run = draw(st.booleans())
        window_mode = draw(_window_mode_st)
        store_id = draw(_store_id_st)
        dwd_only_tables = draw(_dwd_only_tables_st)
        window_split = draw(_window_split_st)
        window_split_days = draw(_window_split_days_st)
        fetch_before_verify = draw(st.booleans())
        skip_ods = draw(st.booleans())
        ods_local = draw(st.booleans())

        if window_mode == "custom":
            d1 = draw(st.dates(
                min_value=datetime.date(2020, 1, 1),
                max_value=datetime.date(2030, 12, 31),
            ))
            d2 = draw(st.dates(
                min_value=datetime.date(2020, 1, 1),
                max_value=datetime.date(2030, 12, 31),
            ))
            # 保证 end >= start
            window_start = min(d1, d2).isoformat()
            window_end = max(d1, d2).isoformat()
            lookback_hours = 24
            overlap_seconds = 600
        else:
            window_start = None
            window_end = None
            lookback_hours = draw(_lookback_hours_st)
            overlap_seconds = draw(_overlap_seconds_st)

        return TaskConfigSchema(
            tasks=tasks,
            flow=flow_id,
            processing_mode=processing_mode,
            dry_run=dry_run,
            window_mode=window_mode,
            window_start=window_start,
            window_end=window_end,
            window_split=window_split,
            window_split_days=window_split_days,
            lookback_hours=lookback_hours,
            overlap_seconds=overlap_seconds,
            fetch_before_verify=fetch_before_verify,
            skip_ods_when_fetch_before_verify=skip_ods,
            ods_use_local_json=ods_local,
            store_id=store_id,
            dwd_only_tables=dwd_only_tables,
            extra_args={},
        )

    return _build()


# ---------------------------------------------------------------------------
# Feature: admin-web-console, Property 1: TaskConfig 序列化往返一致性
# **Validates: Requirements 11.1, 11.2, 11.3**
# ---------------------------------------------------------------------------


@settings(max_examples=200)
@given(config=_valid_task_config_st())
def test_task_config_round_trip(config: TaskConfigSchema):
    """Property 1: 序列化为 JSON 后再反序列化，应产生与原始对象等价的结果。"""
    json_str = config.model_dump_json()
    restored = TaskConfigSchema.model_validate_json(json_str)
    assert restored == config, (
        f"往返不一致：\n原始={config}\n还原={restored}"
    )


# ---------------------------------------------------------------------------
# Feature: admin-web-console, Property 6: 时间窗口验证
# **Validates: Requirements 2.3**
# ---------------------------------------------------------------------------


@settings(max_examples=200)
@given(
    d1=st.dates(
        min_value=datetime.date(2020, 1, 1),
        max_value=datetime.date(2030, 12, 31),
    ),
    d2=st.dates(
        min_value=datetime.date(2020, 1, 1),
        max_value=datetime.date(2030, 12, 31),
    ),
)
def test_time_window_validation(d1: datetime.date, d2: datetime.date):
    """Property 6: window_end < window_start 时验证应失败，否则应通过。"""
    start_str = d1.isoformat()
    end_str = d2.isoformat()

    if end_str < start_str:
        # window_end 早于 window_start → 验证应失败
        try:
            TaskConfigSchema(
                tasks=["ODS_MEMBER"],
                window_mode="custom",
                window_start=start_str,
                window_end=end_str,
            )
            raise AssertionError(
                f"期望 ValidationError，但验证通过了：start={start_str}, end={end_str}"
            )
        except ValidationError:
            pass  # 预期行为
    else:
        # window_end >= window_start → 验证应通过
        config = TaskConfigSchema(
            tasks=["ODS_MEMBER"],
            window_mode="custom",
            window_start=start_str,
            window_end=end_str,
        )
        assert config.window_start == start_str
        assert config.window_end == end_str


# ---------------------------------------------------------------------------
# Feature: admin-web-console, Property 7: TaskConfig 到 CLI 命令转换完整性
# **Validates: Requirements 2.5, 2.6**
# ---------------------------------------------------------------------------

_builder = CLIBuilder()
_ETL_PATH = "/fake/etl/project"


@settings(max_examples=200)
@given(config=_valid_task_config_st())
def test_task_config_to_cli_completeness(config: TaskConfigSchema):
    """Property 7: CLIBuilder 生成的命令应包含 TaskConfig 中所有非空字段对应的 CLI 参数。"""
    cmd = _builder.build_command(config, _ETL_PATH)

    # 1) --flow 始终存在且值正确
    assert "--flow" in cmd
    idx = cmd.index("--flow")
    assert cmd[idx + 1] == config.flow

    # 2) --processing-mode 始终存在且值正确
    assert "--processing-mode" in cmd
    idx = cmd.index("--processing-mode")
    assert cmd[idx + 1] == config.processing_mode

    # 3) 非空任务列表 → --tasks 存在
    if config.tasks:
        assert "--tasks" in cmd
        idx = cmd.index("--tasks")
        assert set(cmd[idx + 1].split(",")) == set(config.tasks)

    # 4) 时间窗口参数
    if config.window_mode == "lookback":
        # lookback 模式 → --lookback-hours 和 --overlap-seconds
        if config.lookback_hours is not None:
            assert "--lookback-hours" in cmd
            idx = cmd.index("--lookback-hours")
            assert cmd[idx + 1] == str(config.lookback_hours)
        if config.overlap_seconds is not None:
            assert "--overlap-seconds" in cmd
            idx = cmd.index("--overlap-seconds")
            assert cmd[idx + 1] == str(config.overlap_seconds)
        # lookback 模式不应出现 custom 参数
        assert "--window-start" not in cmd
        assert "--window-end" not in cmd
    else:
        # custom 模式 → --window-start / --window-end
        if config.window_start:
            assert "--window-start" in cmd
        if config.window_end:
            assert "--window-end" in cmd
        # custom 模式不应出现 lookback 参数
        assert "--lookback-hours" not in cmd
        assert "--overlap-seconds" not in cmd

    # 5) dry_run → --dry-run
    if config.dry_run:
        assert "--dry-run" in cmd
    else:
        assert "--dry-run" not in cmd

    # 6) store_id → --store-id
    if config.store_id is not None:
        assert "--store-id" in cmd
        idx = cmd.index("--store-id")
        assert cmd[idx + 1] == str(config.store_id)
    else:
        assert "--store-id" not in cmd

    # 7) fetch_before_verify → 仅 verify_only 模式下生成
    if config.fetch_before_verify and config.processing_mode == "verify_only":
        assert "--fetch-before-verify" in cmd
    else:
        assert "--fetch-before-verify" not in cmd

    # 8) window_split（非 None 且非 "none"）→ --window-split
    if config.window_split and config.window_split != "none":
        assert "--window-split" in cmd
        idx = cmd.index("--window-split")
        assert cmd[idx + 1] == config.window_split
        if config.window_split_days is not None:
            assert "--window-split-days" in cmd
    else:
        assert "--window-split" not in cmd
