# -*- coding: utf-8 -*-
"""
BaseDwsTask 默认模板方法 — 属性测试 + 单元测试

Feature: etl-dws-flow-refactor
测试位置：apps/etl/connectors/feiqiu/tests/unit/

使用 hypothesis 验证 BaseDwsTask 默认 extract()/load() 的核心正确性属性。
"""
from __future__ import annotations

import sys
from datetime import datetime, timedelta
from pathlib import Path
from types import SimpleNamespace
from typing import Any, Dict, List
from unittest.mock import MagicMock

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.base_task import TaskContext
from tasks.dws.base_dws_task import BaseDwsTask


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

# 合法的 store_id：正整数
st_store_id = st.integers(min_value=1, max_value=10**18)

# 合法的时间窗口：window_start < window_end，带时区
st_window = st.tuples(
    st.datetimes(
        min_value=datetime(2020, 1, 1),
        max_value=datetime(2030, 12, 31),
    ),
    st.integers(min_value=1, max_value=525600),  # 1 分钟 ~ 1 年
).map(
    lambda t: (
        t[0].replace(tzinfo=__import__("zoneinfo").ZoneInfo("Asia/Shanghai")),
        (t[0] + timedelta(minutes=t[1])).replace(
            tzinfo=__import__("zoneinfo").ZoneInfo("Asia/Shanghai")
        ),
        t[1],
    )
)


def build_context(store_id: int, window_start: datetime, window_end: datetime, window_minutes: int) -> TaskContext:
    """构造合法的 TaskContext。"""
    return TaskContext(
        store_id=store_id,
        window_start=window_start,
        window_end=window_end,
        window_minutes=window_minutes,
    )


@st.composite
def st_task_context(draw):
    """生成合法的 TaskContext。"""
    store_id = draw(st_store_id)
    window_start, window_end, window_minutes = draw(st_window)
    return build_context(store_id, window_start, window_end, window_minutes)


# 生成简单的行数据（dict 列表）
st_row = st.fixed_dictionaries({
    "site_id": st_store_id,
    "stat_date": st.dates(),
    "value": st.integers(min_value=0, max_value=10000),
})

st_rows = st.lists(st_row, min_size=0, max_size=20)
st_nonempty_rows = st.lists(st_row, min_size=1, max_size=20)

# DATE_COL 策略：合法的列名标识符
st_date_col = st.sampled_from([
    "stat_date", "visit_date", "stat_month", "salary_month", "created_at",
])


# ============================================================================
# 测试用具体子类
# ============================================================================

class StubDwsTask(BaseDwsTask):
    """用于属性测试的最小 BaseDwsTask 子类。

    声明 DATE_COL，实现 _do_extract()，不覆盖 extract()/load()。
    """

    DATE_COL = "stat_date"

    def __init__(self, date_col: str = "stat_date", extract_rows: list | None = None):
        # 构造最小依赖
        mock_config = MagicMock()
        mock_config.get.side_effect = lambda key, default=None: (
            "Asia/Shanghai" if key == "app.timezone" else default
        )
        mock_db = MagicMock()
        mock_api = MagicMock()
        mock_logger = MagicMock()
        super().__init__(mock_config, mock_db, mock_api, mock_logger)

        # 动态设置 DATE_COL
        self.DATE_COL = date_col
        # _do_extract 返回的预设数据
        self._extract_rows = extract_rows if extract_rows is not None else []

    def get_task_code(self) -> str:
        return "TEST_STUB_DWS"

    def get_target_table(self) -> str:
        return "test_stub_table"

    def get_primary_keys(self) -> list[str]:
        return ["site_id", "stat_date"]

    def _do_extract(self, context: TaskContext) -> list[dict]:
        return self._extract_rows


class OverriddenExtractTask(StubDwsTask):
    """覆盖了 extract() 的子类，用于验证覆盖行为。"""

    def extract(self, context: TaskContext) -> dict:
        return {"custom": True}


# ============================================================================
# Property 1: 默认 extract() 返回标准结构
# Feature: etl-dws-flow-refactor, Property 1: 默认 extract() 返回标准结构
# Validates: Requirements 1.1, 1.4
# ============================================================================

class TestExtractProperty:
    """属性测试：默认 extract() 返回标准结构。"""

    @given(context=st_task_context(), rows=st_rows, date_col=st_date_col)
    @settings(max_examples=100)
    def test_extract_returns_standard_keys(self, context: TaskContext, rows: list, date_col: str):
        """对于任意合法 TaskContext 和 _do_extract 返回值，
        默认 extract() 应返回包含 rows/start_date/end_date/site_id 四个键的字典。

        **Validates: Requirements 1.1, 1.4**
        """
        task = StubDwsTask(date_col=date_col, extract_rows=rows)
        result = task.extract(context)

        # 必须包含四个标准键
        assert set(result.keys()) == {"rows", "start_date", "end_date", "site_id"}

    @given(context=st_task_context(), rows=st_rows, date_col=st_date_col)
    @settings(max_examples=100)
    def test_extract_rows_equals_do_extract(self, context: TaskContext, rows: list, date_col: str):
        """extract() 返回的 rows 应等于 _do_extract(context) 的返回值。

        **Validates: Requirements 1.1, 1.4**
        """
        task = StubDwsTask(date_col=date_col, extract_rows=rows)
        result = task.extract(context)

        assert result["rows"] == rows

    @given(context=st_task_context(), rows=st_rows)
    @settings(max_examples=100)
    def test_extract_dates_from_context(self, context: TaskContext, rows: list):
        """extract() 返回的 start_date/end_date/site_id 应来自 context。

        **Validates: Requirements 1.1, 1.4**
        """
        task = StubDwsTask(extract_rows=rows)
        result = task.extract(context)

        assert result["start_date"] == context.window_start.date()
        assert result["end_date"] == context.window_end.date()
        assert result["site_id"] == context.store_id

    @given(context=st_task_context())
    @settings(max_examples=100)
    def test_extract_none_treated_as_empty(self, context: TaskContext):
        """当 _do_extract 返回 None 时，extract() 应将 rows 设为空列表。

        **Validates: Requirements 1.1, 1.4**
        """
        task = StubDwsTask(extract_rows=None)
        # 覆盖 _do_extract 使其返回 None
        task._do_extract = lambda ctx: None
        result = task.extract(context)

        assert result["rows"] == []


# ============================================================================
# Property 2: 默认 load() 幂等写入与标准统计
# Feature: etl-dws-flow-refactor, Property 2: 默认 load() 幂等写入与标准统计
# Validates: Requirements 1.2, 1.5
# ============================================================================

class TestLoadProperty:
    """属性测试：默认 load() 幂等写入与标准统计。"""

    @given(context=st_task_context(), transformed=st_nonempty_rows, date_col=st_date_col)
    @settings(max_examples=100)
    def test_load_nonempty_returns_standard_counts(
        self, context: TaskContext, transformed: list, date_col: str
    ):
        """对于非空 transformed，load() 应返回包含 counts 键的字典，
        counts 包含 fetched/inserted/updated/skipped/errors 五个整数键，
        且 fetched == len(transformed)。

        **Validates: Requirements 1.2, 1.5**
        """
        task = StubDwsTask(date_col=date_col)

        # mock delete_existing_data 和 bulk_insert，避免真实 DB 操作
        task.delete_existing_data = MagicMock(return_value=len(transformed))
        task.bulk_insert = MagicMock(return_value=len(transformed))

        result = task.load(transformed, context)

        # 必须包含 counts 键
        assert "counts" in result
        counts = result["counts"]

        # counts 必须包含五个标准键
        expected_keys = {"fetched", "inserted", "updated", "skipped", "errors"}
        assert set(counts.keys()) == expected_keys

        # 所有值必须是整数
        for key in expected_keys:
            assert isinstance(counts[key], int), f"counts[{key}] 应为 int，实际为 {type(counts[key])}"

        # fetched 等于 len(transformed)
        assert counts["fetched"] == len(transformed)

    @given(context=st_task_context())
    @settings(max_examples=100)
    def test_load_empty_returns_zero_counts(self, context: TaskContext):
        """对于空 transformed，load() 应返回所有计数为 0 的字典。

        **Validates: Requirements 1.2, 1.5**
        """
        task = StubDwsTask()
        result = task.load([], context)

        assert "counts" in result
        counts = result["counts"]
        expected_keys = {"fetched", "inserted", "updated", "skipped", "errors"}
        assert set(counts.keys()) == expected_keys

        for key in expected_keys:
            assert counts[key] == 0

    @given(context=st_task_context(), transformed=st_nonempty_rows, date_col=st_date_col)
    @settings(max_examples=100)
    def test_load_calls_delete_then_insert(
        self, context: TaskContext, transformed: list, date_col: str
    ):
        """非空 transformed 时，load() 应先调用 delete_existing_data 再调用 bulk_insert。

        **Validates: Requirements 1.2, 1.5**
        """
        call_order = []
        task = StubDwsTask(date_col=date_col)

        def mock_delete(ctx, date_col=None):
            call_order.append("delete")
            return 0

        def mock_insert(rows):
            call_order.append("insert")
            return len(rows)

        task.delete_existing_data = mock_delete
        task.bulk_insert = mock_insert

        task.load(transformed, context)

        assert call_order == ["delete", "insert"]

    @given(context=st_task_context(), transformed=st_nonempty_rows, date_col=st_date_col)
    @settings(max_examples=100)
    def test_load_uses_date_col(
        self, context: TaskContext, transformed: list, date_col: str
    ):
        """load() 应使用 DATE_COL 作为 delete_existing_data 的 date_col 参数。

        **Validates: Requirements 1.2, 1.5**
        """
        captured_date_col = []
        task = StubDwsTask(date_col=date_col)

        def mock_delete(ctx, date_col=None):
            captured_date_col.append(date_col)
            return 0

        task.delete_existing_data = mock_delete
        task.bulk_insert = MagicMock(return_value=len(transformed))

        task.load(transformed, context)

        assert len(captured_date_col) == 1
        assert captured_date_col[0] == date_col

    @given(context=st_task_context(), transformed=st_nonempty_rows)
    @settings(max_examples=100)
    def test_load_none_date_col_falls_back(self, context: TaskContext, transformed: list):
        """DATE_COL 为 None 时，load() 应回退到 "stat_date"。

        **Validates: Requirements 1.2, 1.5**
        """
        captured_date_col = []
        task = StubDwsTask()
        task.DATE_COL = None  # 显式设为 None

        def mock_delete(ctx, date_col=None):
            captured_date_col.append(date_col)
            return 0

        task.delete_existing_data = mock_delete
        task.bulk_insert = MagicMock(return_value=len(transformed))

        task.load(transformed, context)

        assert captured_date_col[0] == "stat_date"


# ============================================================================
# 单元测试：覆盖行为验证（Requirements 1.3）
# ============================================================================

class TestOverrideBehavior:
    """验证子类覆盖 extract()/load() 时使用子类实现。"""

    def test_overridden_extract_used(self):
        """覆盖了 extract() 的子类应使用自定义实现。"""
        ctx = build_context(
            store_id=123,
            window_start=datetime(2026, 1, 1, tzinfo=__import__("zoneinfo").ZoneInfo("Asia/Shanghai")),
            window_end=datetime(2026, 1, 2, tzinfo=__import__("zoneinfo").ZoneInfo("Asia/Shanghai")),
            window_minutes=1440,
        )
        task = OverriddenExtractTask()
        result = task.extract(ctx)
        assert result == {"custom": True}

    def test_not_implemented_do_extract_raises(self):
        """未实现 _do_extract 且未覆盖 extract 的子类应抛出 NotImplementedError。"""

        class BareTask(BaseDwsTask):
            def get_task_code(self): return "BARE"
            def get_target_table(self): return "bare_table"
            def get_primary_keys(self): return ["id"]

        mock_config = MagicMock()
        mock_config.get.side_effect = lambda key, default=None: (
            "Asia/Shanghai" if key == "app.timezone" else default
        )
        task = BareTask(mock_config, MagicMock(), MagicMock(), MagicMock())
        ctx = build_context(
            store_id=1,
            window_start=datetime(2026, 1, 1, tzinfo=__import__("zoneinfo").ZoneInfo("Asia/Shanghai")),
            window_end=datetime(2026, 1, 2, tzinfo=__import__("zoneinfo").ZoneInfo("Asia/Shanghai")),
            window_minutes=1440,
        )
        with pytest.raises(NotImplementedError):
            task.extract(ctx)
