#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：execution_service.py
模块描述：执行服务，负责测试任务的调度、执行和结果收集

作者：czj
创建日期：2026-07-09
最后修改：2026-07-09
"""

import asyncio
import logging
import subprocess
import sys
import os
import json
import threading
from pathlib import Path
from typing import Optional, List, Tuple, Dict, Any, Callable
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor

from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, desc, or_

from app.models.execution import Execution
from app.models.case_result import CaseResult
from app.models.test_case import TestCase
from app.models.module import Module
from app.executors.playwright_executor import (
    PlaywrightExecutor,
    StepResult,
    CaseExecutionResult,
)
from app.websocket.manager import manager
from app.utils.id_generator import generate_id
from app.config import settings

logger = logging.getLogger(__name__)

# ==================== 全局执行锁 ====================
# 确保 UI 自动化用例串行执行，避免并发执行导致浏览器页面状态互相干扰
# 根因：Playwright 浏览器实例共享页面状态，并发执行会导致状态竞争
_execution_lock = threading.Lock()
# 正在运行的执行任务ID（全局 UI 执行锁）
_running_execution_id: Optional[str] = None
_running_execution_lock = threading.Lock()


def is_execution_running() -> Tuple[bool, Optional[str]]:
    """
    检查是否有执行任务正在运行

    Returns:
        Tuple[bool, Optional[str]]: (是否有任务运行, 运行中的执行ID)
    """
    with _running_execution_lock:
        return _running_execution_id is not None, _running_execution_id


def set_running_execution(execution_id: Optional[str]) -> None:
    """
    设置/清除当前运行中的执行ID

    同时维护取消请求集合：设置新执行时清空旧的取消标记。

    Args:
        execution_id: 执行ID，None 表示清除
    """
    global _running_execution_id
    with _running_execution_lock:
        _running_execution_id = execution_id
        if execution_id:
            logger.info(f"[执行锁] 设置运行中: {execution_id}")
        else:
            logger.info("[执行锁] 清除运行状态")


# 被请求取消的执行ID集合（供 Playwright 工作线程快速退出循环）
_cancel_requested: set = set()
_cancel_lock = threading.Lock()


def request_cancel(execution_id: str) -> None:
    """
    请求取消指定执行（线程安全）

    仅标记取消意图，不阻塞调用方；Playwright 工作线程在用例/步骤边界检查
    该标记并提前退出，从而使 run_execution 尽快返回、释放全局锁。

    Args:
        execution_id (str): 执行记录ID
    """
    with _cancel_lock:
        _cancel_requested.add(execution_id)


def is_cancel_requested(execution_id: str) -> bool:
    """
    检查指定执行是否被请求取消（线程安全）

    Args:
        execution_id (str): 执行记录ID

    Returns:
        bool: 是否已请求取消
    """
    with _cancel_lock:
        return execution_id in _cancel_requested


def clear_cancel_requested(execution_id: str) -> None:
    """
    清除指定执行的取消请求标记

    在创建新执行或执行重跑时调用，避免旧取消标记残留导致新执行一启动就被中断。

    Args:
        execution_id (str): 执行记录ID
    """
    with _cancel_lock:
        _cancel_requested.discard(execution_id)


class ExecutionService:
    """
    执行服务类

    负责测试执行的调度、执行和结果收集管理。
    支持单个用例执行、批量执行、模块执行和全量执行。

    Attributes:
        db (AsyncSession): 数据库会话对象

    Example:
        >>> service = ExecutionService(db)
        >>> execution = await service.create_execution(case_ids=["case_001"])
        >>> await service.run_execution(execution.id)
    """

    def __init__(self, db: AsyncSession):
        """
        初始化执行服务

        Args:
            db (AsyncSession): 数据库会话对象
        """
        self.db = db

    async def list_executions(
        self,
        skip: int = 0,
        limit: int = 50,
        status: Optional[str] = None,
        case_type: Optional[str] = None,
        trigger_type: Optional[str] = None,
        trigger_by: Optional[str] = None,
    ) -> Tuple[List[Execution], int]:
        """
        获取执行记录列表

        Args:
            skip (int): 跳过记录数
            limit (int): 返回记录数上限
            status (Optional[str]): 状态筛选
            case_type (Optional[str]): 用例类型筛选 (ui/api/security/deploy/performance/functional)
            trigger_type (Optional[str]): 触发类型筛选 (manual/scheduled)
            trigger_by (Optional[str]): 触发人/任务名筛选

        Returns:
            Tuple[List[Execution], int]: (执行记录列表, 总数)
        """
        conditions = []
        if status:
            conditions.append(Execution.status == status)
        if case_type:
            conditions.append(Execution.case_type == case_type)
        if trigger_type:
            conditions.append(Execution.trigger_type == trigger_type)
        if trigger_by:
            conditions.append(Execution.trigger_by == trigger_by)

        count_query = select(func.count(Execution.id))
        if conditions:
            count_query = count_query.where(*conditions)
        total_result = await self.db.execute(count_query)
        total = total_result.scalar() or 0

        query = (
            select(Execution)
            .order_by(desc(Execution.created_at))
            .offset(skip)
            .limit(limit)
        )
        if conditions:
            query = query.where(*conditions)

        result = await self.db.execute(query)
        return list(result.scalars().all()), total

    async def get_execution(self, execution_id: str) -> Optional[Execution]:
        """
        获取执行记录详情

        Args:
            execution_id (str): 执行记录ID

        Returns:
            Optional[Execution]: 执行记录对象
        """
        query = select(Execution).where(Execution.id == execution_id)
        result = await self.db.execute(query)
        return result.scalar_one_or_none()

    async def create_execution(
        self,
        case_ids: List[str],
        name: str = "",
        trigger_type: str = "manual",
        trigger_by: str = "user",
        environment: str = "default",
        config: Optional[dict] = None,
    ) -> Execution:
        """
        创建执行记录

        Args:
            case_ids (List[str]): 要执行的用例ID列表
            name (str): 执行名称
            trigger_type (str): 触发类型
            trigger_by (str): 触发人
            environment (str): 执行环境
            config (Optional[dict]): 执行配置

        Returns:
            Execution: 执行记录对象
        """
        # 生成执行名称
        if not name:
            date_str = datetime.now().strftime("%Y%m%d-%H%M%S")
            name = f"测试执行-{date_str}"

        # 先查询用例，取首个用例的 case_type 作为执行记录的类型（用于按类型筛选）
        # 按模块分组 + order 排序，确保同模块内首用例（含登录）先执行
        cases_query = select(TestCase).where(TestCase.id.in_(case_ids)).order_by(
            TestCase.module_id, TestCase.order, TestCase.created_at
        )
        cases_result = await self.db.execute(cases_query)
        cases = list(cases_result.scalars().all())
        first_case_type = cases[0].case_type if cases else "ui"

        execution = Execution(
            id=generate_id("exec"),
            name=name,
            trigger_type=trigger_type,
            trigger_by=trigger_by,
            total_cases=len(case_ids),
            status="pending",
            environment=environment,
            config=config or {},
            case_type=first_case_type,
        )

        self.db.add(execution)
        await self.db.flush()
        await self.db.refresh(execution)

        # 创建用例结果记录
        for case in cases:
            case_result = CaseResult(
                id=generate_id("result"),
                execution_id=execution.id,
                case_id=case.id,
                case_name=case.name,
                status="pending",
            )
            self.db.add(case_result)

        await self.db.flush()
        logger.info(f"执行记录创建成功: id={execution.id}, name={name}, cases={len(case_ids)}, case_type={first_case_type}")
        return execution

    async def run_execution(
        self,
        execution_id: str,
        config: Optional[dict] = None,
    ) -> Execution:
        """
        执行测试任务

        异步执行所有关联的测试用例，实时推送执行状态。
        UI 自动化用例串行执行，避免并发干扰。

        Args:
            execution_id (str): 执行记录ID
            config (Optional[dict]): 执行配置

        Returns:
            Execution: 更新后的执行记录

        Raises:
            ValueError: 执行记录不存在或已有其他任务运行中
        """
        execution = await self.get_execution(execution_id)
        if not execution:
            raise ValueError(f"执行记录不存在: {execution_id}")

        # ===== 按用例类型分流：安全测试走专用执行器 =====
        if execution.case_type == "security":
            return await self._run_security_execution(execution_id, config)

        # ===== UI 自动化用例串行执行锁检查 =====
        # 检查是否有其他 UI 执行任务正在运行
        is_running, running_id = is_execution_running()
        if is_running and running_id != execution_id:
            error_msg = f"已有 UI 执行任务 {running_id} 正在运行，请等待完成后再执行"
            logger.warning(f"[执行锁] {error_msg}")
            execution.status = "failed"
            execution.error_message = error_msg
            execution.end_time = datetime.now()
            await self.db.flush()
            raise ValueError(error_msg)

        # 获取执行锁
        set_running_execution(execution_id)

        # 清除旧取消标记（防止重跑时残留）
        clear_cancel_requested(execution_id)

        # 更新状态为运行中并立即提交：
        # 若不提交，长执行期间事务会一直持有 executions 行锁（MySQL InnoDB），
        # 取消接口的 UPDATE 将被阻塞（Lock wait timeout exceeded 1205），
        # 导致执行中无法取消。立即提交可释放行锁，让取消接口随时可写。
        execution.status = "running"
        execution.start_time = datetime.now()
        await self.db.commit()
        # commit 后 ORM 实例已过期，重新查询刷新，避免后续异步属性访问触发刷新
        execution = await self.get_execution(execution_id)

        # 广播执行开始
        await manager.broadcast(execution_id, {
            "type": "execution_start",
            "data": execution.to_dict(),
        })

        # 获取所有待执行的用例结果
        # JOIN TestCase 按模块分组 + order 排序，确保同模块内首用例（含登录）先执行
        results_query = (
            select(CaseResult)
            .join(TestCase, CaseResult.case_id == TestCase.id)
            .where(
                CaseResult.execution_id == execution_id,
                CaseResult.status == "pending",
            )
            .order_by(TestCase.module_id, TestCase.order, TestCase.created_at)
        )
        results_data = await self.db.execute(results_query)
        case_results = list(results_data.scalars().all())

        # 获取用例定义
        case_ids = [r.case_id for r in case_results]
        cases_query = select(TestCase).where(TestCase.id.in_(case_ids))
        cases_data = await self.db.execute(cases_query)
        cases_map = {c.id: c for c in cases_data.scalars().all()}

        # 在线程池中为每个用例使用独立执行器
        loop = asyncio.get_event_loop()

        async def _persist_case_result(case_result_obj, result):
            """单个用例执行完毕：立即写库 + 更新统计 + 广播（跨线程提交到事件循环）"""
            try:
                # 已取消 → 不覆盖 skipped 状态，直接跳过写入
                status_check = await self.db.execute(
                    select(Execution.status).where(Execution.id == execution_id)
                )
                if status_check.scalar_one_or_none() == "cancelled":
                    return

                case_result_obj.status = result.status
                case_result_obj.duration = result.duration
                case_result_obj.end_time = datetime.now()
                case_result_obj.retry_count = result.retry_count
                case_result_obj.error_message = result.error_message
                case_result_obj.steps_result = [
                    {"order": s.order, "name": s.name, "action": s.action,
                     "status": s.status, "duration": s.duration,
                     "screenshot": s.screenshot, "log": s.log, "error": s.error}
                    for s in result.steps_result
                ]
                execution.passed = await self._count_results(execution_id, "passed")
                execution.failed = await self._count_results(execution_id, "failed")
                execution.skipped = await self._count_results(execution_id, "skipped")
                execution.pass_rate = execution.calculate_pass_rate()
                await self.db.commit()  # 立即提交，前端轮询可见

                await manager.broadcast(execution_id, {
                    "type": "case_complete",
                    "data": case_result_obj.to_dict(),
                })
            except Exception as e:
                logger.error(f"实时写入用例结果失败: {e}")
                await self.db.rollback()

        # 定义一个共享的执行函数，复用同一个执行器
        def run_all_cases_sync(cases_list, exec_config, ev_loop, broadcast_fn, result_callback):
            """
            在一个执行器中执行所有用例，保持登录态

            Args:
                cases_list: (case_result_obj, case_dict) 元组列表
                exec_config: 执行配置
                ev_loop: 主事件循环（用于线程安全广播）
                broadcast_fn: 异步广播函数（async broadcast）
                result_callback: 用例完成后的实时写库回调（async）
            """
            logger.info(f"[run_all_cases_sync] 开始执行, 用例数: {len(cases_list)}")

            executor_config = exec_config or {}
            # auto_login 由用例自身配置决定，不强制覆盖
            # 透传单步重试次数（默认 2）
            executor_config.setdefault("step_retry_count", exec_config.get("step_retry_count", 2) if exec_config else 2)
            # 传入被测系统 base_url（从配置动态获取，确保切换目标环境后无需改代码）
            executor_config.setdefault("base_url", settings.TARGET_URL)

            executor = PlaywrightExecutor(config=executor_config)
            # 传入 execution_id，供 execute_step 检查取消请求
            executor._execution_id = execution_id
            results = []

            try:
                executor.start()
                logger.info(f"[run_all_cases_sync] 执行器启动成功")

                # 第一个用例会自动登录
                for i, (case_result_obj, case_dict) in enumerate(cases_list):
                    # ★ 取消请求检查（用例边界）：取消后立即中断剩余用例，
                    # 使 run_execution 尽快返回并释放全局锁，不再后台空跑数小时。
                    if is_cancel_requested(execution_id):
                        logger.info(
                            f"[run_all_cases_sync] 检测到取消请求，中断剩余用例 "
                            f"({i}/{len(cases_list)} 已执行)"
                        )
                        break

                    case_id = case_dict.get("id", "")
                    case_name = case_dict.get("name", "")
                    steps_count = len(case_dict.get("steps", []))
                    logger.info(f"⏳ 执行用例 {i+1}/{len(cases_list)}: {case_name} (steps={steps_count})")

                    # 构造线程安全的步骤回调（跨线程提交到事件循环）
                    def step_callback(step_result, _cid=case_id, _cname=case_name):
                        try:
                            asyncio.run_coroutine_threadsafe(
                                broadcast_fn(execution_id, {
                                    "type": "step_update",
                                    "data": {
                                        "execution_id": execution_id,
                                        "case_id": _cid,
                                        "case_name": _cname,
                                        "step_order": step_result.order,
                                        "step_name": step_result.name,
                                        "action": step_result.action,
                                        "status": step_result.status,
                                        "duration": step_result.duration,
                                        "screenshot": step_result.screenshot,
                                        "log": step_result.log,
                                        "error": step_result.error,
                                    },
                                }),
                                ev_loop,
                            )
                        except Exception as e:
                            logger.error(f"✗ 步骤回调提交失败: {str(e)}")

                    result = executor.execute_case(case=case_dict, callback=step_callback)
                    logger.info(f"[run_all_cases_sync] 用例 {case_name} 执行完成, status={result.status}")
                    results.append(result)

                    # ★ 实时写库：跨线程提交到事件循环（不阻塞 Playwright 线程）
                    try:
                        asyncio.run_coroutine_threadsafe(
                            result_callback(case_result_obj, result),
                            ev_loop,
                        )
                    except Exception as e:
                        logger.error(f"✗ 用例结果回调提交失败: {str(e)}")

                logger.info(f"[run_all_cases_sync] 所有用例执行完成, 结果数: {len(results)}")
                return results
            except Exception as e:
                logger.error(f"[run_all_cases_sync] 异常: {type(e).__name__}: {e}")
                raise
            finally:
                logger.info("[run_all_cases_sync] 停止执行器")
                executor.stop()

        try:
            # 准备用例数据列表
            cases_to_run = []
            for case_result in case_results:
                case = cases_map.get(case_result.case_id)
                if case:
                    cases_to_run.append({
                        "case_result": case_result,
                        "case_dict": case.to_dict(),
                    })

            # 用专用线程执行 Playwright，避免 asyncio 线程池复用线程导致 greenlet 死锁。
            # 根因：sync_playwright().start() 在 ThreadPoolExecutor 复用的 worker 线程上
            # 会永久卡死（容器内已多次复现：仅打印「开始执行」，永不打印「执行器启动成功」）。
            # 专用线程不关联任何 asyncio 事件循环，sync API 可正常启动；
            # 步骤回调已通过 asyncio.run_coroutine_threadsafe(..., ev_loop) 跨线程回桥。
            exec_holder = {}  # {"data": [...]} 或 {"err": Exception}

            def _playwright_worker():
                try:
                    exec_holder["data"] = run_all_cases_sync(
                        [(c["case_result"], c["case_dict"]) for c in cases_to_run],
                        config,
                        loop,
                        manager.broadcast,
                        _persist_case_result,
                    )
                except Exception as e:  # noqa: BLE001 - 线程内异常需透传到主协程
                    exec_holder["err"] = e

            worker_thread = threading.Thread(
                target=_playwright_worker,
                name=f"playwright-exec-{execution_id}",
                daemon=True,
            )
            worker_thread.start()
            # 主协程挂起等待专用线程结束（run_in_executor 仅用于 join，不执行 Playwright）
            await loop.run_in_executor(None, worker_thread.join)

            if "err" in exec_holder:
                raise exec_holder["err"]
            all_results = exec_holder.get("data", [])

            # ★ 实时写入模式：每个用例的结果已在 _persist_case_result 中逐条 commit，
            # 此处不再批量写库（原来 join 后再批量写导致执行期间 DB 无任何更新）。
            # 取消场景：cancel_execution 已把未执行用例标记 skipped；
            # _persist_case_result 写入前也会检查 cancelled 状态，不会覆盖 skipped。

        except Exception as e:
            logger.error(f"执行过程异常: {str(e)}")
            remaining = await self._get_pending_results(execution_id)
            for r in remaining:
                r.status = "failed"
                r.error_message = f"执行过程异常: {str(e)}"
            await self.db.flush()

        finally:
            # 释放执行锁
            set_running_execution(None)

            # 读取最新执行状态（避免使用 commit 后的过期实例）：
            # 执行期间可能已被用户取消，若已取消则保持 cancelled，不被覆盖为 completed。
            # 必须用【列查询（标量）+ FOR UPDATE 当前读】：
            #   1. MySQL REPEATABLE READ 快照读看不到本事务之外已提交的取消；
            #   2. 完整实体查询会命中 SQLAlchemy 身份映射，返回内存中过期的 execution
            #      实例（expire_on_commit=False，commit 后不刷新），FOR UPDATE 读了数据库
            #      也不灌入对象，.status 仍是旧值 running（已实机复现）。
            final_q = await self.db.execute(
                select(Execution.status).where(Execution.id == execution_id).with_for_update()
            )
            final_status = final_q.scalar_one_or_none()
            if final_status is None:
                return execution
            was_cancelled = final_status == "cancelled"
            if was_cancelled:
                logger.info(f"执行已在运行中被取消，保持 cancelled: {execution_id}")

            # 用当前会话中的 execution 实例写回（内存字段可能过期，此处显式覆盖全部字段）
            execution.status = "cancelled" if was_cancelled else "completed"
            execution.end_time = datetime.now()
            execution.duration = (execution.end_time - execution.start_time).total_seconds()
            execution.passed = await self._count_results(execution_id, "passed")
            execution.failed = await self._count_results(execution_id, "failed")
            execution.skipped = await self._count_results(execution_id, "skipped")
            execution.pass_rate = execution.calculate_pass_rate()
            await self.db.flush()

            # 广播执行完成
            await manager.broadcast(execution_id, {
                "type": "execution_complete",
                "data": execution.to_dict(),
            })

            logger.info(
                f"执行完成: id={execution_id}, "
                f"通过={execution.passed}, 失败={execution.failed}, "
                f"耗时={execution.duration:.2f}s"
            )

        return execution

    async def cancel_execution(self, execution_id: str) -> bool:
        """
        取消执行任务

        Args:
            execution_id (str): 执行记录ID

        Returns:
            bool: 取消是否成功
        """
        execution = await self.get_execution(execution_id)
        if not execution:
            return False

        if execution.status not in ("pending", "running"):
            raise ValueError(f"执行任务状态为 {execution.status}，无法取消")

        execution.status = "cancelled"
        execution.end_time = datetime.now()
        await self.db.flush()

        # ★ 请求取消：置位取消标记，Playwright 工作线程在用例/步骤边界检查并提前退出，
        # 使 run_execution 尽快返回、释放全局锁，不再后台空跑剩余用例。
        request_cancel(execution_id)

        # 标记剩余待执行的用例为跳过
        remaining = await self._get_pending_results(execution_id)
        for r in remaining:
            r.status = "skipped"
            r.error_message = "执行任务已取消"
        await self.db.flush()

        # 广播取消
        await manager.broadcast(execution_id, {
            "type": "execution_cancelled",
            "data": execution.to_dict(),
        })

        logger.info(f"执行已取消: {execution_id}")
        return True

    async def delete_execution(self, execution_id: str) -> bool:
        """
        删除执行记录（级联删除关联的用例结果，不影响测试用例）

        执行记录与测试用例是独立的：删除执行记录只丢失本次执行的
        结果快照，用例定义本身（TestCase）不受影响，可再次执行。

        Args:
            execution_id (str): 执行记录ID

        Returns:
            bool: 删除是否成功

        Raises:
            ValueError: 执行记录不存在或正在运行中
        """
        execution = await self.get_execution(execution_id)
        if not execution:
            raise ValueError(f"执行记录不存在: {execution_id}")

        # 运行中的执行记录禁止删除，避免影响正在执行的任务
        if execution.status == "running":
            raise ValueError(f"执行记录正在运行中，无法删除: {execution_id}")

        # 删除（ORM 级联自动删除关联的 CaseResult，不影响 TestCase）
        await self.db.delete(execution)
        await self.db.flush()
        logger.info(f"执行记录删除成功: id={execution_id}")
        return True

    async def delete_executions(
        self, execution_ids: List[str]
    ) -> Tuple[int, int, int, List[str]]:
        """
        批量删除执行记录

        Args:
            execution_ids (List[str]): 执行记录ID列表

        Returns:
            Tuple[int, int, int, List[str]]: (成功数, 失败数, 跳过数, 错误信息列表)
        """
        success = 0
        failed = 0
        skipped = 0
        errors: List[str] = []
        for eid in execution_ids:
            try:
                await self.delete_execution(eid)
                success += 1
            except ValueError as e:
                msg = str(e)
                if "运行中" in msg:
                    skipped += 1
                else:
                    failed += 1
                    errors.append(f"{eid}: {msg}")
            except Exception as e:
                failed += 1
                errors.append(f"{eid}: {str(e)}")
        await self.db.commit()
        logger.info(f"批量删除执行记录完成: 成功={success}, 失败={failed}, 跳过={skipped}")
        return success, failed, skipped, errors

    async def get_results(
        self, execution_id: str
    ) -> Tuple[List[CaseResult], int]:
        """
        获取执行结果列表

        Args:
            execution_id (str): 执行记录ID

        Returns:
            Tuple[List[CaseResult], int]: (用例结果列表, 总数)
        """
        query = (
            select(CaseResult)
            .where(CaseResult.execution_id == execution_id)
            .order_by(CaseResult.created_at.asc())
        )
        result = await self.db.execute(query)
        items = list(result.scalars().all())

        return items, len(items)

    async def get_case_result(
        self, execution_id: str, case_id: str
    ) -> Optional[CaseResult]:
        """
        获取单个用例的执行结果

        Args:
            execution_id (str): 执行记录ID
            case_id (str): 用例ID

        Returns:
            Optional[CaseResult]: 用例结果对象
        """
        query = select(CaseResult).where(
            CaseResult.execution_id == execution_id,
            CaseResult.case_id == case_id,
        )
        result = await self.db.execute(query)
        return result.scalar_one_or_none()

    def _create_step_callback(
        self, execution_id: str, case_result: CaseResult
    ) -> Callable[[StepResult], None]:
        """
        创建步骤回调函数

        用于在步骤完成时实时推送状态到 WebSocket 客户端。

        Args:
            execution_id (str): 执行记录ID
            case_result (CaseResult): 当前用例结果

        Returns:
            Callable: 回调函数
        """
        def callback(step_result: StepResult) -> None:
            """步骤完成回调"""
            try:
                # 创建异步任务发送消息
                asyncio.create_task(
                    manager.broadcast(execution_id, {
                        "type": "step_update",
                        "data": {
                            "execution_id": execution_id,
                            "case_id": case_result.case_id,
                            "case_name": case_result.case_name,
                            "step_order": step_result.order,
                            "step_name": step_result.name,
                            "action": step_result.action,
                            "status": step_result.status,
                            "duration": step_result.duration,
                            "screenshot": step_result.screenshot,
                            "log": step_result.log,
                            "error": step_result.error,
                        },
                    })
                )
            except Exception as e:
                logger.error(f"步骤回调执行失败: {str(e)}")

        return callback

    async def _run_security_execution(
        self,
        execution_id: str,
        config: Optional[dict] = None,
    ) -> Execution:
        """
        安全测试专用执行流程

        安全测试用例不使用 Playwright（无浏览器），而是用 requests + 签名算法
        直接调用被测系统 API。本方法：
        1. 从执行记录中取出关联的用例
        2. 加载默认安全测试配置
        3. 用 SecurityExecutor 执行
        4. 把结果写回 CaseResult + VulnerabilityResult

        Args:
            execution_id: 执行记录ID
            config: 执行配置（未使用，保留兼容）

        Returns:
            Execution: 更新后的执行记录
        """
        # 延迟导入，避免循环依赖
        from app.executors.security_executor import SecurityExecutor, SecurityCaseResult
        from app.models.security_config import SecurityConfig
        from app.models.vulnerability_result import VulnerabilityResult

        execution = await self.get_execution(execution_id)
        if not execution:
            raise ValueError(f"执行记录不存在: {execution_id}")

        # 广播执行开始
        await manager.broadcast(execution_id, {
            "type": "execution_start",
            "data": execution.to_dict(),
        })

        # 获取所有待执行的用例结果（按模块分组排序）
        results_query = (
            select(CaseResult)
            .join(TestCase, CaseResult.case_id == TestCase.id)
            .where(
                CaseResult.execution_id == execution_id,
                CaseResult.status == "pending",
            )
            .order_by(TestCase.module_id, TestCase.order, TestCase.created_at)
        )
        results_data = await self.db.execute(results_query)
        case_results = list(results_data.scalars().all())

        # 获取用例定义
        case_ids = [r.case_id for r in case_results]
        cases_query = select(TestCase).where(TestCase.id.in_(case_ids))
        cases_data = await self.db.execute(cases_query)
        cases = list(cases_data.scalars().all())
        cases_map = {c.id: c for c in cases}

        # 加载默认安全测试配置
        cfg_query = select(SecurityConfig).where(SecurityConfig.is_default == True)
        cfg_result = await self.db.execute(cfg_query)
        sec_config = cfg_result.scalar_one_or_none()
        if not sec_config:
            # 没有配置，全部标记失败
            for cr in case_results:
                cr.status = "error"
                cr.error_message = "未找到默认安全测试配置"
            execution.status = "failed"
            execution.end_time = datetime.now()
            await self.db.flush()
            return execution

        client_config = sec_config.to_client_config()

        # 在 async 上下文中预先把 ORM 对象转成纯字典（避免线程池脱钩）
        def _to_plain_steps(steps_val):
            if isinstance(steps_val, str):
                try:
                    parsed = json.loads(steps_val)
                    return parsed if isinstance(parsed, dict) else {}
                except Exception:
                    return {}
            return steps_val if isinstance(steps_val, dict) else {}

        plain_cases = [
            {"id": c.id, "name": c.name, "steps": _to_plain_steps(c.steps)}
            for c in cases
        ]

        # 定义线程安全的同步执行函数
        loop = asyncio.get_event_loop()

        def sync_run_security():
            executor = SecurityExecutor(client_config)
            executor.start()
            results = []
            for i, case_dict in enumerate(plain_cases):
                case_id = case_dict.get("id", "")
                case_name = case_dict.get("name", "")
                logger.info(f"⏳ 安全测试用例 {i+1}/{len(plain_cases)}: {case_name}")
                try:
                    result = executor.execute_case(case_dict)
                except Exception as e:
                    logger.error(f"安全用例执行异常: {case_name}, {e}")
                    result = SecurityCaseResult(
                        case_id=case_id, name=case_name,
                        status="error", error=str(e),
                    )
                results.append(result)
            return results

        try:
            results: List[SecurityCaseResult] = await loop.run_in_executor(
                None, sync_run_security
            )
        except Exception as e:
            logger.error(f"安全测试执行异常: {e}")
            execution.status = "failed"
            execution.end_time = datetime.now()
            await self.db.flush()
            return execution

        # 更新用例结果 + 保存漏洞结果
        passed = 0
        failed = 0
        for r in results:
            # 找到对应的 CaseResult 更新状态
            cr = next((c for c in case_results if c.case_id == r.case_id), None)
            if cr:
                if r.status == "error":
                    cr.status = "error"
                    cr.error_message = r.error
                elif r.is_vulnerable:
                    cr.status = "failed"
                    cr.error_message = r.description[:500] if r.description else None
                    failed += 1
                else:
                    cr.status = "passed"
                    passed += 1
                cr.duration = r.duration
                cr.end_time = datetime.now()

            # 保存漏洞测试结果
            vuln_result = VulnerabilityResult(
                id=generate_id("vuln"),
                execution_id=execution_id,
                case_id=r.case_id,
                test_id=r.test_id,
                name=r.name,
                level=r.level,
                description=r.description,
                request_info=r.request_info,
                response_info=r.response_info,
                is_vulnerable=r.is_vulnerable,
                fix_suggestion=r.fix_suggestion,
                duration=r.duration,
                status=r.status,
                error_message=r.error,
                extra_data=r.metadata,
            )
            self.db.add(vuln_result)

        # 更新执行统计
        execution.passed = passed
        execution.failed = failed
        execution.skipped = len(case_results) - passed - failed
        execution.total_cases = len(case_results)
        execution.status = "completed"
        execution.end_time = datetime.now()
        execution.duration = (execution.end_time - execution.start_time).total_seconds() if execution.start_time else 0
        execution.pass_rate = round((passed / execution.total_cases) * 100, 2) if execution.total_cases > 0 else 0

        await self.db.flush()

        # 广播执行完成
        await manager.broadcast(execution_id, {
            "type": "execution_complete",
            "data": execution.to_dict(),
        })

        logger.info(f"安全测试执行完成: {execution_id}, 通过: {passed}, 失败: {failed}")
        return execution

    async def _count_results(self, execution_id: str, status: str) -> int:
        """
        统计指定状态的用例结果数量

        Args:
            execution_id (str): 执行记录ID
            status (str): 状态

        Returns:
            int: 数量
        """
        query = select(func.count(CaseResult.id)).where(
            CaseResult.execution_id == execution_id,
            CaseResult.status == status,
        )
        result = await self.db.execute(query)
        return result.scalar() or 0

    async def _get_pending_results(
        self, execution_id: str
    ) -> List[CaseResult]:
        """
        获取待执行的用例结果

        Args:
            execution_id (str): 执行记录ID

        Returns:
            List[CaseResult]: 待执行的用例结果列表
        """
        query = select(CaseResult).where(
            CaseResult.execution_id == execution_id,
            CaseResult.status.in_(["pending", "running"]),
        )
        result = await self.db.execute(query)
        return list(result.scalars().all())