# -*- coding: utf-8 -*-
"""门店隔离属性测试（Property-Based Testing）。

Property 20: 对于任意两个不同 site_id 的 Operator，一个 Operator 查询
队列/调度/执行历史时，结果中不应包含另一个 site_id 的数据。

Validates: Requirements 1.3

测试策略：
- 通过 mock 数据库交互，验证 API 路由在不同 site_id 下的数据隔离
- 队列隔离：为 site_id_a 入队任务，用 site_id_b 的 JWT 查询队列，结果应为空
- 调度隔离：为 site_id_a 创建调度任务，用 site_id_b 的 JWT 查询调度列表，结果应为空
- 执行历史隔离：site_id_a 的执行历史，用 site_id_b 的 JWT 查询不到
"""

import os

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

import json
import uuid
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 fastapi.testclient import TestClient

from app.auth.dependencies import CurrentUser, get_current_user
from app.main import app


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

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


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

def _make_mock_user(site_id: int) -> CurrentUser:
    """构造指定 site_id 的 mock 用户。"""
    return CurrentUser(user_id=1, site_id=site_id)


def _make_queue_rows(site_id: int, count: int) -> list[tuple]:
    """生成 count 条属于 site_id 的队列行。"""
    rows = []
    for i in range(count):
        rows.append((
            str(uuid.uuid4()),                                    # id
            site_id,                                              # site_id
            json.dumps({"tasks": ["ODS_MEMBER"], "flow": "api_ods"}),  # config
            "pending",                                            # status
            i + 1,                                                # position
            datetime(2024, 1, 1, tzinfo=timezone.utc),            # created_at
            None,                                                 # started_at
            None,                                                 # finished_at
            None,                                                 # exit_code
            None,                                                 # error_message
        ))
    return rows


def _make_schedule_rows(site_id: int, count: int) -> list[tuple]:
    """生成 count 条属于 site_id 的调度行。"""
    now = datetime.now(timezone.utc)
    rows = []
    for i in range(count):
        rows.append((
            str(uuid.uuid4()),                                    # id
            site_id,                                              # site_id
            f"调度任务_{i}",                                       # name
            ["ODS_MEMBER"],                                       # task_codes
            {"tasks": ["ODS_MEMBER"], "flow": "api_ods"},     # task_config
            {"schedule_type": "daily", "daily_time": "04:00",     # schedule_config
             "interval_value": 1, "interval_unit": "hours",
             "weekly_days": [1], "weekly_time": "04:00",
             "cron_expression": "0 4 * * *", "enabled": True,
             "start_date": None, "end_date": None},
            True,                                                 # enabled
            None,                                                 # last_run_at
            now + timedelta(hours=1),                             # next_run_at
            0,                                                    # run_count
            None,                                                 # last_status
            now,                                                  # created_at
            now,                                                  # updated_at
        ))
    return rows


def _make_history_rows(site_id: int, count: int) -> list[tuple]:
    """生成 count 条属于 site_id 的执行历史行。"""
    base_time = datetime(2024, 1, 1, tzinfo=timezone.utc)
    rows = []
    for i in range(count):
        rows.append((
            str(uuid.uuid4()),                                    # id
            site_id,                                              # site_id
            ["ODS_MEMBER"],                                       # task_codes
            "success",                                            # status
            base_time + timedelta(hours=i),                       # started_at
            base_time + timedelta(hours=i, minutes=30),           # finished_at
            0,                                                    # exit_code
            1800000,                                              # duration_ms
            "python -m cli.main",                                 # command
            None,                                                 # summary
        ))
    return rows


def _mock_conn_returning(rows: list[tuple]) -> MagicMock:
    """构造一个 mock connection，其 cursor.fetchall 返回指定行。"""
    mock_cursor = MagicMock()
    mock_cursor.fetchall.return_value = rows
    mock_conn = MagicMock()
    mock_conn.cursor.return_value.__enter__ = MagicMock(return_value=mock_cursor)
    mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
    return mock_conn


# ---------------------------------------------------------------------------
# Property 20.1: 队列隔离
# **Validates: Requirements 1.3**
# ---------------------------------------------------------------------------

@settings(max_examples=100, deadline=None)
@given(
    site_id_a=_site_id_st,
    site_id_b=_site_id_st,
    queue_count=st.integers(min_value=1, max_value=5),
)
@patch("app.services.task_queue.get_connection")
def test_queue_isolation(mock_get_conn, site_id_a, site_id_b, queue_count):
    """Property 20.1: 队列隔离。

    为 site_id_a 入队若干任务后，用 site_id_b 的身份查询队列，
    结果应为空——不同门店的队列数据互不可见。
    """
    assume(site_id_a != site_id_b)

    # site_id_a 的队列数据
    rows_a = _make_queue_rows(site_id_a, queue_count)

    # 核心隔离逻辑：根据查询时传入的 site_id 过滤
    # list_pending 内部 SQL: WHERE site_id = %s AND status = 'pending'
    def conn_for_site(querying_site_id):
        """模拟数据库行为：只返回匹配 site_id 的行。"""
        if querying_site_id == site_id_a:
            return rows_a
        return []  # site_id_b 查不到 site_id_a 的数据

    captured_params = {}

    def make_mock_conn():
        mock_cursor = MagicMock()

        def execute_side_effect(sql, params=None):
            if params:
                captured_params["site_id"] = params[0]
                # 根据 SQL 中的 site_id 参数返回对应数据
                mock_cursor.fetchall.return_value = conn_for_site(params[0])

        mock_cursor.execute = MagicMock(side_effect=execute_side_effect)
        mock_cursor.__enter__ = MagicMock(return_value=mock_cursor)
        mock_cursor.__exit__ = MagicMock(return_value=False)

        mock_conn = MagicMock()
        mock_conn.cursor.return_value = mock_cursor
        return mock_conn

    mock_get_conn.return_value = make_mock_conn()

    # 用 site_id_b 的身份查询队列
    app.dependency_overrides[get_current_user] = lambda: _make_mock_user(site_id_b)
    try:
        client = TestClient(app)
        resp = client.get("/api/execution/queue")

        assert resp.status_code == 200
        data = resp.json()

        # 验证：site_id_b 查不到 site_id_a 的任何数据
        assert len(data) == 0, (
            f"site_id_b={site_id_b} 不应看到 site_id_a={site_id_a} 的队列数据，"
            f"但返回了 {len(data)} 条记录"
        )

        # 额外验证：即使有数据返回，也不应包含 site_id_a 的记录
        for item in data:
            assert item.get("site_id") != site_id_a, (
                f"结果中不应包含 site_id_a={site_id_a} 的数据"
            )
    finally:
        app.dependency_overrides.clear()


# ---------------------------------------------------------------------------
# Property 20.2: 调度隔离
# **Validates: Requirements 1.3**
# ---------------------------------------------------------------------------

@settings(max_examples=100)
@given(
    site_id_a=_site_id_st,
    site_id_b=_site_id_st,
    schedule_count=st.integers(min_value=1, max_value=5),
)
@patch("app.routers.schedules.get_connection")
def test_schedule_isolation(mock_get_conn, site_id_a, site_id_b, schedule_count):
    """Property 20.2: 调度隔离。

    为 site_id_a 创建若干调度任务后，用 site_id_b 的身份查询调度列表，
    结果应为空——不同门店的调度数据互不可见。
    """
    assume(site_id_a != site_id_b)

    # site_id_a 的调度数据
    rows_a = _make_schedule_rows(site_id_a, schedule_count)

    def make_mock_conn():
        mock_cursor = MagicMock()

        def execute_side_effect(sql, params=None):
            if params:
                querying_site_id = params[0]
                # 只返回匹配 site_id 的行
                if querying_site_id == site_id_a:
                    mock_cursor.fetchall.return_value = rows_a
                else:
                    mock_cursor.fetchall.return_value = []

        mock_cursor.execute = MagicMock(side_effect=execute_side_effect)
        mock_cursor.__enter__ = MagicMock(return_value=mock_cursor)
        mock_cursor.__exit__ = MagicMock(return_value=False)

        mock_conn = MagicMock()
        mock_conn.cursor.return_value = mock_cursor
        return mock_conn

    mock_get_conn.return_value = make_mock_conn()

    # 用 site_id_b 的身份查询调度列表
    app.dependency_overrides[get_current_user] = lambda: _make_mock_user(site_id_b)
    try:
        client = TestClient(app)
        resp = client.get("/api/schedules")

        assert resp.status_code == 200
        data = resp.json()

        # 验证：site_id_b 查不到 site_id_a 的任何调度数据
        assert len(data) == 0, (
            f"site_id_b={site_id_b} 不应看到 site_id_a={site_id_a} 的调度数据，"
            f"但返回了 {len(data)} 条记录"
        )

        # 额外验证：即使有数据返回，也不应包含 site_id_a 的记录
        for item in data:
            assert item.get("site_id") != site_id_a, (
                f"结果中不应包含 site_id_a={site_id_a} 的调度数据"
            )
    finally:
        app.dependency_overrides.clear()


# ---------------------------------------------------------------------------
# Property 20.3: 执行历史隔离
# **Validates: Requirements 1.3**
# ---------------------------------------------------------------------------

@settings(max_examples=100, deadline=None)
@given(
    site_id_a=_site_id_st,
    site_id_b=_site_id_st,
    history_count=st.integers(min_value=1, max_value=10),
)
@patch("app.routers.execution.get_connection")
def test_execution_history_isolation(mock_get_conn, site_id_a, site_id_b, history_count):
    """Property 20.3: 执行历史隔离。

    site_id_a 有若干执行历史记录，用 site_id_b 的身份查询执行历史，
    结果应为空——不同门店的执行历史互不可见。
    """
    assume(site_id_a != site_id_b)

    # site_id_a 的执行历史数据
    rows_a = _make_history_rows(site_id_a, history_count)

    def make_mock_conn():
        mock_cursor = MagicMock()

        def execute_side_effect(sql, params=None):
            if params:
                querying_site_id = params[0]
                # 只返回匹配 site_id 的行
                if querying_site_id == site_id_a:
                    mock_cursor.fetchall.return_value = rows_a
                else:
                    mock_cursor.fetchall.return_value = []

        mock_cursor.execute = MagicMock(side_effect=execute_side_effect)
        mock_cursor.__enter__ = MagicMock(return_value=mock_cursor)
        mock_cursor.__exit__ = MagicMock(return_value=False)

        mock_conn = MagicMock()
        mock_conn.cursor.return_value = mock_cursor
        return mock_conn

    mock_get_conn.return_value = make_mock_conn()

    # 用 site_id_b 的身份查询执行历史
    app.dependency_overrides[get_current_user] = lambda: _make_mock_user(site_id_b)
    try:
        client = TestClient(app)
        resp = client.get("/api/execution/history")

        assert resp.status_code == 200
        data = resp.json()

        # 验证：site_id_b 查不到 site_id_a 的任何执行历史
        assert len(data) == 0, (
            f"site_id_b={site_id_b} 不应看到 site_id_a={site_id_a} 的执行历史，"
            f"但返回了 {len(data)} 条记录"
        )

        # 额外验证：即使有数据返回，也不应包含 site_id_a 的记录
        for item in data:
            assert item.get("site_id") != site_id_a, (
                f"结果中不应包含 site_id_a={site_id_a} 的执行历史"
            )
    finally:
        app.dependency_overrides.clear()
