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

使用 hypothesis 验证调度管理的通用正确性属性：
- Property 12: 调度任务 CRUD 往返
- Property 13: 到期调度任务自动入队
- Property 14: 调度任务启用/禁用状态

测试策略：
- Property 12: 通过 mock 数据库，验证 POST 创建后 GET 返回的 schedule_config 与提交的一致
- Property 13: 通过 mock 数据库返回到期任务，验证 check_and_enqueue 调用了 task_queue.enqueue
- Property 14: 通过 mock 数据库，验证 toggle 端点的 next_run_at 行为
"""

import os

os.environ.setdefault("JWT_SECRET_KEY", "test-secret-key-for-schedule-properties")

import json
from datetime import datetime, timedelta, timezone
from unittest.mock import MagicMock, patch

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

from app.auth.dependencies import CurrentUser, get_current_user
from app.main import app
from app.schemas.schedules import ScheduleConfigSchema
from app.schemas.tasks import TaskConfigSchema
from app.services.scheduler import Scheduler, calculate_next_run

from fastapi.testclient import TestClient


# ---------------------------------------------------------------------------
# 通用策略（Strategies）
# ---------------------------------------------------------------------------

_site_id_st = st.integers(min_value=1, max_value=2**31 - 1)

_task_codes = ["ODS_MEMBER", "ODS_PAYMENT", "ODS_ORDER", "DWD_LOAD_FROM_ODS", "DWS_SUMMARY"]

_simple_task_config_st = st.fixed_dictionaries({
    "tasks": st.lists(st.sampled_from(_task_codes), min_size=1, max_size=3, unique=True),
    "flow": st.sampled_from(["api_ods", "api_ods_dwd", "ods_dwd", "api_full"]),
})

# 调度配置策略：覆盖 5 种调度类型
_schedule_type_st = st.sampled_from(["once", "interval", "daily", "weekly", "cron"])

_interval_unit_st = st.sampled_from(["minutes", "hours", "days"])

# HH:MM 格式的时间字符串
_time_str_st = st.builds(
    lambda h, m: f"{h:02d}:{m:02d}",
    h=st.integers(min_value=0, max_value=23),
    m=st.integers(min_value=0, max_value=59),
)

# ISO weekday 列表（1=Monday ... 7=Sunday）
_weekly_days_st = st.lists(
    st.integers(min_value=1, max_value=7),
    min_size=1, max_size=7, unique=True,
)

# 简单 cron 表达式（minute hour * * *）
_cron_st = st.builds(
    lambda m, h: f"{m} {h} * * *",
    m=st.integers(min_value=0, max_value=59),
    h=st.integers(min_value=0, max_value=23),
)


def _build_schedule_config(schedule_type, interval_value, interval_unit,
                           daily_time, weekly_days, weekly_time, cron_expression):
    """根据 schedule_type 构建 ScheduleConfigSchema。"""
    return ScheduleConfigSchema(
        schedule_type=schedule_type,
        interval_value=interval_value,
        interval_unit=interval_unit,
        daily_time=daily_time,
        weekly_days=weekly_days,
        weekly_time=weekly_time,
        cron_expression=cron_expression,
        enabled=True,
    )


_schedule_config_st = st.builds(
    _build_schedule_config,
    schedule_type=_schedule_type_st,
    interval_value=st.integers(min_value=1, max_value=168),
    interval_unit=_interval_unit_st,
    daily_time=_time_str_st,
    weekly_days=_weekly_days_st,
    weekly_time=_time_str_st,
    cron_expression=_cron_st,
)

# 用于 Property 14 的非 once 调度配置（启用后 next_run_at 应非 NULL）
_non_once_schedule_type_st = st.sampled_from(["interval", "daily", "weekly", "cron"])

_non_once_schedule_config_st = st.builds(
    _build_schedule_config,
    schedule_type=_non_once_schedule_type_st,
    interval_value=st.integers(min_value=1, max_value=168),
    interval_unit=_interval_unit_st,
    daily_time=_time_str_st,
    weekly_days=_weekly_days_st,
    weekly_time=_time_str_st,
    cron_expression=_cron_st,
)


# ---------------------------------------------------------------------------
# 辅助函数
# ---------------------------------------------------------------------------

_NOW = datetime(2025, 6, 10, 10, 0, 0, tzinfo=timezone.utc)

# 模拟数据库行的列顺序（与 _SELECT_COLS 对应，共 13 列）
# id, site_id, name, task_codes, task_config, schedule_config,
# enabled, last_run_at, next_run_at, run_count, last_status,
# created_at, updated_at


def _make_db_row(
    schedule_id: str,
    site_id: int,
    name: str,
    task_codes: list[str],
    task_config: dict,
    schedule_config: dict,
    enabled: bool = True,
    next_run_at: datetime | None = None,
) -> tuple:
    """构造模拟数据库行。"""
    return (
        schedule_id, site_id, name, task_codes,
        json.dumps(task_config) if isinstance(task_config, dict) else task_config,
        json.dumps(schedule_config) if isinstance(schedule_config, dict) else schedule_config,
        enabled, None, next_run_at, 0, None, _NOW, _NOW,
    )


# ---------------------------------------------------------------------------
# Feature: admin-web-console, Property 12: 调度任务 CRUD 往返
# **Validates: Requirements 5.1, 5.4**
# ---------------------------------------------------------------------------

@settings(max_examples=100)
@given(
    site_id=_site_id_st,
    schedule_config=_schedule_config_st,
    task_config=_simple_task_config_st,
    name=st.text(min_size=1, max_size=50, alphabet=st.characters(
        whitelist_categories=("L", "N"), whitelist_characters="_- "
    )),
    task_codes=st.lists(st.sampled_from(_task_codes), min_size=1, max_size=3, unique=True),
)
@patch("app.routers.schedules.get_connection")
def test_schedule_crud_round_trip(
    mock_get_conn, site_id, schedule_config, task_config, name, task_codes,
):
    """Property 12: 调度任务 CRUD 往返。

    有效的 ScheduleConfigSchema，创建调度任务后再查询该任务，
    返回的调度配置应与创建时提交的配置等价。
    """
    schedule_config_dict = schedule_config.model_dump()
    next_run = calculate_next_run(schedule_config, _NOW)

    # 构造创建后数据库返回的行
    created_row = _make_db_row(
        schedule_id="test-sched-id",
        site_id=site_id,
        name=name,
        task_codes=task_codes,
        task_config=task_config,
        schedule_config=schedule_config_dict,
        enabled=schedule_config.enabled,
        next_run_at=next_run,
    )

    # --- 创建阶段 ---
    # mock POST 的数据库连接（INSERT ... RETURNING）
    create_cursor = MagicMock()
    create_cursor.fetchone.return_value = created_row
    create_conn = MagicMock()
    create_conn.cursor.return_value.__enter__ = MagicMock(return_value=create_cursor)
    create_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)

    # --- 查询阶段 ---
    # mock GET 的数据库连接（SELECT ... fetchall）
    list_cursor = MagicMock()
    list_cursor.fetchall.return_value = [created_row]
    list_conn = MagicMock()
    list_conn.cursor.return_value.__enter__ = MagicMock(return_value=list_cursor)
    list_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)

    # 依次返回 create_conn 和 list_conn
    mock_get_conn.side_effect = [create_conn, list_conn]

    # 覆盖认证
    test_user = CurrentUser(user_id=1, site_id=site_id)
    app.dependency_overrides[get_current_user] = lambda: test_user

    try:
        client = TestClient(app)

        # 创建调度任务
        create_body = {
            "name": name,
            "task_codes": task_codes,
            "task_config": task_config,
            "schedule_config": schedule_config_dict,
        }
        create_resp = client.post("/api/schedules", json=create_body)
        assert create_resp.status_code == 201, (
            f"创建应返回 201，实际 {create_resp.status_code}: {create_resp.text}"
        )
        created_data = create_resp.json()

        # 查询调度任务列表
        list_resp = client.get("/api/schedules")
        assert list_resp.status_code == 200
        list_data = list_resp.json()

        assert len(list_data) >= 1, "查询结果应至少包含刚创建的任务"

        # 找到刚创建的任务
        found = next((s for s in list_data if s["id"] == created_data["id"]), None)
        assert found is not None, "查询结果应包含刚创建的任务"

        # 核心验证：schedule_config 往返一致
        returned_config = found["schedule_config"]
        for key in schedule_config_dict:
            assert returned_config[key] == schedule_config_dict[key], (
                f"schedule_config.{key} 不一致："
                f"提交={schedule_config_dict[key]}，返回={returned_config[key]}"
            )

        # 验证 task_config 往返一致
        returned_task_config = found["task_config"]
        for key in task_config:
            assert returned_task_config[key] == task_config[key], (
                f"task_config.{key} 不一致：提交={task_config[key]}，返回={returned_task_config[key]}"
            )

        # 验证基本字段
        assert found["name"] == name
        assert found["task_codes"] == task_codes
        assert found["site_id"] == site_id
    finally:
        app.dependency_overrides[get_current_user] = lambda: CurrentUser(user_id=1, site_id=100)



# ---------------------------------------------------------------------------
# Feature: admin-web-console, Property 13: 到期调度任务自动入队
# **Validates: Requirements 5.2**
# ---------------------------------------------------------------------------

@settings(max_examples=100)
@given(
    site_id=_site_id_st,
    schedule_config=_schedule_config_st,
    task_config=_simple_task_config_st,
)
@patch("app.services.scheduler.task_queue")
@patch("app.services.scheduler.get_connection")
def test_due_schedule_auto_enqueue(
    mock_get_conn, mock_tq, site_id, schedule_config, task_config,
):
    """Property 13: 到期调度任务自动入队。

    enabled 为 true 且 next_run_at 早于当前时间的调度任务，
    check_and_enqueue 执行后该任务的 TaskConfig 应出现在执行队列中。
    """
    sched = Scheduler()
    schedule_config_dict = schedule_config.model_dump()

    # 构造到期任务：next_run_at 在过去（比 now 早 5 分钟）
    task_id = "due-task-001"

    # --- mock SELECT 到期任务 ---
    select_cursor = MagicMock()
    select_cursor.fetchall.return_value = [
        (task_id, site_id, json.dumps(task_config), json.dumps(schedule_config_dict)),
    ]
    select_cursor.__enter__ = MagicMock(return_value=select_cursor)
    select_cursor.__exit__ = MagicMock(return_value=False)

    # --- mock UPDATE 调度状态 ---
    update_cursor = MagicMock()
    update_cursor.__enter__ = MagicMock(return_value=update_cursor)
    update_cursor.__exit__ = MagicMock(return_value=False)

    conn = MagicMock()
    conn.cursor.side_effect = [select_cursor, update_cursor]
    mock_get_conn.return_value = conn

    mock_tq.enqueue.return_value = "queue-id-123"

    # 执行
    count = sched.check_and_enqueue()

    # 验证：到期任务被入队
    assert count == 1, f"应有 1 个任务入队，实际 {count}"
    mock_tq.enqueue.assert_called_once()

    # 验证入队参数
    call_args = mock_tq.enqueue.call_args
    enqueued_config = call_args[0][0]
    enqueued_site_id = call_args[0][1]

    # site_id 应匹配
    assert enqueued_site_id == site_id, (
        f"入队的 site_id 应为 {site_id}，实际 {enqueued_site_id}"
    )

    # TaskConfig 应与原始配置一致
    assert isinstance(enqueued_config, TaskConfigSchema)
    assert enqueued_config.tasks == task_config["tasks"], (
        f"入队的 tasks 应为 {task_config['tasks']}，实际 {enqueued_config.tasks}"
    )
    assert enqueued_config.flow == task_config["flow"], (
        f"入队的 flow 应为 {task_config['flow']}，实际 {enqueued_config.flow}"
    )


# ---------------------------------------------------------------------------
# Feature: admin-web-console, Property 14: 调度任务启用/禁用状态
# **Validates: Requirements 5.3**
# ---------------------------------------------------------------------------

@settings(max_examples=100, deadline=None)
@given(
    site_id=_site_id_st,
    schedule_config=_non_once_schedule_config_st,
    task_config=_simple_task_config_st,
    name=st.text(min_size=1, max_size=30, alphabet=st.characters(
        whitelist_categories=("L", "N"), whitelist_characters="_- "
    )),
    task_codes=st.lists(st.sampled_from(_task_codes), min_size=1, max_size=3, unique=True),
)
@patch("app.routers.schedules.get_connection")
def test_schedule_toggle_next_run(
    mock_get_conn, site_id, schedule_config, task_config, name, task_codes,
):
    """Property 14: 调度任务启用/禁用状态。

    禁用后 next_run_at 应为 NULL；
    重新启用后 next_run_at 应被重新计算为非 NULL 值（对于非一次性调度）。
    """
    schedule_config_dict = schedule_config.model_dump()
    next_run_enabled = calculate_next_run(schedule_config, _NOW)

    # --- 第一步：禁用（enabled=True → False）---
    # toggle 端点先 SELECT 当前状态，再 UPDATE RETURNING

    # 禁用后的数据库行
    disabled_row = _make_db_row(
        schedule_id="sched-toggle-1",
        site_id=site_id,
        name=name,
        task_codes=task_codes,
        task_config=task_config,
        schedule_config=schedule_config_dict,
        enabled=False,
        next_run_at=None,  # 禁用后 next_run_at 为 NULL
    )

    # mock 禁用操作的数据库连接
    disable_cursor = MagicMock()
    disable_cursor.fetchone.side_effect = [
        (True, json.dumps(schedule_config_dict)),  # SELECT 当前状态（enabled=True）
        disabled_row,                               # UPDATE RETURNING
    ]
    disable_conn = MagicMock()
    disable_conn.cursor.return_value.__enter__ = MagicMock(return_value=disable_cursor)
    disable_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)

    # --- 第二步：启用（enabled=False → True）---
    enabled_row = _make_db_row(
        schedule_id="sched-toggle-1",
        site_id=site_id,
        name=name,
        task_codes=task_codes,
        task_config=task_config,
        schedule_config=schedule_config_dict,
        enabled=True,
        next_run_at=next_run_enabled,  # 启用后 next_run_at 被重新计算
    )

    enable_cursor = MagicMock()
    enable_cursor.fetchone.side_effect = [
        (False, json.dumps(schedule_config_dict)),  # SELECT 当前状态（enabled=False）
        enabled_row,                                 # UPDATE RETURNING
    ]
    enable_conn = MagicMock()
    enable_conn.cursor.return_value.__enter__ = MagicMock(return_value=enable_cursor)
    enable_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)

    # 依次返回两个连接
    mock_get_conn.side_effect = [disable_conn, enable_conn]

    # 覆盖认证
    test_user = CurrentUser(user_id=1, site_id=site_id)
    app.dependency_overrides[get_current_user] = lambda: test_user

    try:
        client = TestClient(app)

        # 禁用
        disable_resp = client.patch("/api/schedules/sched-toggle-1/toggle")
        assert disable_resp.status_code == 200, (
            f"禁用应返回 200，实际 {disable_resp.status_code}: {disable_resp.text}"
        )
        disable_data = disable_resp.json()

        # 验证：禁用后 enabled=False，next_run_at=NULL
        assert disable_data["enabled"] is False, "禁用后 enabled 应为 False"
        assert disable_data["next_run_at"] is None, "禁用后 next_run_at 应为 NULL"

        # 启用
        enable_resp = client.patch("/api/schedules/sched-toggle-1/toggle")
        assert enable_resp.status_code == 200, (
            f"启用应返回 200，实际 {enable_resp.status_code}: {enable_resp.text}"
        )
        enable_data = enable_resp.json()

        # 验证：启用后 enabled=True，next_run_at 非 NULL（非一次性调度）
        assert enable_data["enabled"] is True, "启用后 enabled 应为 True"
        assert enable_data["next_run_at"] is not None, (
            "非一次性调度启用后 next_run_at 应被重新计算为非 NULL 值"
        )
    finally:
        app.dependency_overrides[get_current_user] = lambda: CurrentUser(user_id=1, site_id=100)
