#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：api_test_executor.py
模块描述：接口测试执行引擎 — 发送 HTTP 请求并执行断言，验证接口边界/异常/参数组合

核心设计：
- 每个用例 = 一次 HTTP 请求 + N 条断言规则
- 复用 HttpClient（登录 Token / 签名 / SSL 处理）
- 断言类型：status_code / body_field / response_time / header
- 支持模板变量替换（复用 TemplateResolver）

作者：czj
创建日期：2026-08-17
"""

import json
import logging
import time
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional, List, Dict, Any

from app.executors.http_client import HttpClient
from app.executors.auth_helper import AuthHelper
from app.executors.template_resolver import TemplateResolver

logger = logging.getLogger(__name__)


# ==================== 数据类 ====================

@dataclass
class ApiAssertionResult:
    """单条断言执行结果"""
    type: str = ""
    operator: str = ""
    expected: Any = None
    actual: Any = None
    passed: bool = False
    message: str = ""

    def to_dict(self) -> dict:
        return {
            "type": self.type,
            "operator": self.operator,
            "expected": self.expected,
            "actual": self.actual,
            "passed": self.passed,
            "message": self.message,
        }


@dataclass
class ApiStepResult:
    """
    单次 API 请求执行结果

    Attributes:
        order (int): 步骤序号
        name (str): 用例名称
        method (str): 请求方法
        url (str): 请求完整 URL
        status_code (int): HTTP 状态码
        response_time (float): 响应时间（ms）
        request_headers (dict): 请求头
        request_body (Any): 请求体
        response_headers (dict): 响应头
        response_body (str): 响应体文本
        assertions (list): 断言执行结果列表
        passed (bool): 是否全部断言通过
        error (str): 请求错误信息
    """
    order: int = 0
    name: str = ""
    method: str = ""
    url: str = ""
    status_code: int = 0
    response_time: float = 0.0
    request_headers: Dict[str, str] = field(default_factory=dict)
    request_body: Any = None
    response_headers: Dict[str, str] = field(default_factory=dict)
    response_body: str = ""
    assertions: List[ApiAssertionResult] = field(default_factory=list)
    passed: bool = False
    error: str = ""

    def to_dict(self) -> dict:
        return {
            "order": self.order,
            "name": self.name,
            "method": self.method,
            "url": self.url,
            "status_code": self.status_code,
            "response_time": self.response_time,
            "request_headers": self.request_headers,
            "request_body": self.request_body,
            "response_headers": self.response_headers,
            "response_body": self.response_body,
            "assertions": [a.to_dict() for a in self.assertions],
            "passed": self.passed,
            "error": self.error,
        }


@dataclass
class ApiCaseResult:
    """
    单个接口测试用例执行结果

    Attributes:
        case_id (str): 用例 ID
        case_name (str): 用例名称
        status (str): passed/failed/skipped
        duration (float): 耗时（秒）
        step (ApiStepResult): 请求步骤结果
        error (str): 错误信息
    """
    case_id: str = ""
    case_name: str = ""
    status: str = "pending"
    duration: float = 0.0
    step: Optional[ApiStepResult] = None
    error: str = ""

    def to_dict(self) -> dict:
        return {
            "case_id": self.case_id,
            "case_name": self.case_name,
            "status": self.status,
            "duration": self.duration,
            "step": self.step.to_dict() if self.step else None,
            "error": self.error,
        }


# ==================== 断言引擎 ====================

class AssertionEngine:
    """
    断言引擎

    执行断言规则列表，判断一次 HTTP 请求结果是否符合预期。
    支持五种断言类型：
    - status_code: HTTP 状态码比较
    - response_time: 响应时间比较（ms）
    - body_field: 响应体 JSONPath 字段检查（支持 $.a.b[0].c 和 $.a.b）
    - body_not_contains: 响应体原始文本不包含指定字符串（数据自清理无残留校验）
    - header: 响应头检查
    """

    @staticmethod
    def validate(assertions: List[dict], step: ApiStepResult) -> List[ApiAssertionResult]:
        """
        执行所有断言，返回每条断言的详细结果

        Args:
            assertions: 断言规则列表
            step: 请求执行结果

        Returns:
            list[ApiAssertionResult]: 每条断言的执行结果
        """
        results: List[ApiAssertionResult] = []

        if not assertions:
            # 无断言时：默认状态码 2xx 为通过
            passed = 200 <= step.status_code < 300
            results.append(ApiAssertionResult(
                type="status_code",
                operator="default_2xx",
                expected="2xx",
                actual=step.status_code,
                passed=passed,
                message=f"无断言规则，默认校验状态码 2xx（实际 {step.status_code}）" if not passed else "无断言规则，默认通过（2xx）",
            ))
            return results

        for assertion in assertions:
            result = AssertionEngine._evaluate(assertion, step)
            results.append(result)

        return results

    @staticmethod
    def is_passed(results: List[ApiAssertionResult]) -> bool:
        """判断是否所有断言通过"""
        return all(r.passed for r in results)

    @staticmethod
    def _evaluate(assertion: dict, step: ApiStepResult) -> ApiAssertionResult:
        """
        评估单条断言

        Args:
            assertion: 断言规则字典 {type, operator, value, path}
            step: 请求执行结果

        Returns:
            ApiAssertionResult: 断言执行结果
        """
        atype = assertion.get("type", "")
        operator = assertion.get("operator", "equals")
        expected = assertion.get("value")

        result = ApiAssertionResult(
            type=atype,
            operator=operator,
            expected=expected,
        )

        try:
            if atype == "status_code":
                actual = step.status_code
                result.actual = actual
                result.passed = AssertionEngine._compare(actual, operator, expected)
                result.message = f"状态码断言: 期望{operator} {expected}, 实际 {actual}"

            elif atype == "response_time":
                actual = step.response_time
                result.actual = actual
                result.passed = AssertionEngine._compare(actual, operator, expected)
                result.message = f"响应时间断言: 期望{operator} {expected}ms, 实际 {actual:.0f}ms"

            elif atype == "body_field":
                path = assertion.get("path", "")
                result.actual = AssertionEngine._extract_json_path(step.response_body, path)
                result.passed = AssertionEngine._check_value(result.actual, operator, expected)
                result.message = f"响应体断言 [{path}]: 期望{operator} {expected}, 实际 {result.actual}"

            elif atype == "body_not_contains":
                # 数据自清理：原始响应体文本不应包含指定字符串（如已取消的会议名）
                result.actual = step.response_body
                expected_str = str(expected) if expected is not None else ""
                # expected 为空（如 {__CTX:key__} 未渲染）视为失败，避免假通过
                result.passed = bool(expected is not None and expected_str not in (result.actual or ""))
                result.message = f"响应体不应包含 [{expected_str}]"

            elif atype == "header":
                path = assertion.get("path", "Content-Type")
                actual = step.response_headers.get(path, "")
                result.actual = actual
                if operator == "exists":
                    result.passed = path in step.response_headers
                else:
                    result.passed = AssertionEngine._check_value(actual, operator, expected)
                result.message = f"响应头断言 [{path}]: 期望{operator} {expected}, 实际 {actual}"

            else:
                # 未知断言类型默认通过
                result.passed = True
                result.message = f"未知断言类型: {atype}，默认通过"

        except Exception as e:
            result.passed = False
            result.message = f"断言执行异常: {e}"

        return result

    @staticmethod
    def _compare(actual: Any, operator: str, expected: Any) -> bool:
        """
        值比较

        Args:
            actual: 实际值
            operator: 运算符
            expected: 期望值

        Returns:
            bool: 比较结果
        """
        try:
            if operator == "equals":
                # 数值比较：尝试转 float（避免 "200" vs 200 类型不一致）
                try:
                    return float(actual) == float(expected)
                except (TypeError, ValueError):
                    return actual == expected
            elif operator == "not_equals":
                try:
                    return float(actual) != float(expected)
                except (TypeError, ValueError):
                    return actual != expected
            elif operator == "less_than":
                return float(actual) < float(expected)
            elif operator == "less_or_equal":
                return float(actual) <= float(expected)
            elif operator == "greater_than":
                return float(actual) > float(expected)
            elif operator == "greater_or_equal":
                return float(actual) >= float(expected)
            elif operator == "contains":
                return str(expected) in str(actual)
            elif operator == "regex":
                import re
                return re.search(str(expected), str(actual)) is not None
            return True
        except (TypeError, ValueError) as e:
            logger.warning(f"断言比较异常: {e}")
            return False

    @staticmethod
    def _check_value(actual: Any, operator: str, expected: Any) -> bool:
        """
        检查值是否符合预期（用于 body_field/header 断言）

        当实际值为 None 时视为失败（字段不存在），除非运算符为 contains 且期望值也为空。

        Args:
            actual: 实际值
            operator: 运算符
            expected: 期望值

        Returns:
            bool: 检查结果
        """
        if actual is None:
            # 字段不存在
            if operator == "not_exists":
                return True
            return False

        return AssertionEngine._compare(actual, operator, expected)

    @staticmethod
    def _extract_json_path(body_text: str, path: str) -> Any:
        """
        从响应体 JSON 中按路径提取值

        支持路径格式：
        - $.code
        - $.data.messageId
        - $.data.records[0].id
        - code / data.messageId（省略 $ 前缀）

        Args:
            body_text: 响应体文本
            path: JSONPath 字符串

        Returns:
            Any: 提取的值，不存在返回 None
        """
        if not body_text or not path:
            return None

        try:
            data = json.loads(body_text)
            # 去掉 $ 前缀和点
            clean_path = path.strip()
            if clean_path.startswith("$"):
                clean_path = clean_path[1:]
            clean_path = clean_path.lstrip(".")
            if not clean_path:
                return data

            parts = clean_path.split(".")
            current: Any = data
            for part in parts:
                # 处理数组索引：records[0]
                if "[" in part and part.endswith("]"):
                    name, idx_str = part.split("[", 1)
                    idx = int(idx_str[:-1])
                    if name:
                        current = current.get(name) if isinstance(current, dict) else None
                    if current is None:
                        return None
                    if isinstance(current, list) and len(current) > idx:
                        current = current[idx]
                    else:
                        return None
                else:
                    current = current.get(part) if isinstance(current, dict) else None
                if current is None:
                    return None
            return current
        except (json.JSONDecodeError, ValueError):
            # 非 JSON 响应体，尝试原始文本包含匹配
            return None


# ==================== 执行器 ====================

class ApiTestExecutor:
    """
    接口测试执行器

    不依赖浏览器，使用 requests 库发送 HTTP 请求，执行断言。

    Usage:
        >>> executor = ApiTestExecutor(client_config)
        >>> executor.start()
        >>> results = executor.execute_cases(cases)  # cases: [{id, name, steps}]
        >>> executor.stop()
    """

    def __init__(self, client_config: Dict[str, Any]):
        """
        初始化接口测试执行器

        Args:
            client_config: HttpClient 配置字典（兼容 SecurityConfig.to_client_config() 格式）
        """
        self.config = client_config
        self.client = HttpClient(client_config)
        self.auth = AuthHelper(self.client)
        self._is_running = False

    def start(self) -> bool:
        """
        初始化执行环境，预登录账号

        Returns:
            bool: 是否成功初始化（登录失败不阻塞，接口可无认证测试）
        """
        logger.info("初始化接口测试执行环境...")

        try:
            tokens = self.auth.login_all_accounts()
            success_count = sum(1 for v in tokens.values() if v)
            logger.info(f"预登录完成: {success_count}/{len(tokens)} 个账号成功")
        except Exception as e:
            logger.warning(f"预登录失败（不阻塞执行）: {e}")

        self._is_running = True
        return True

    def stop(self):
        """停止执行"""
        self._is_running = False
        logger.info("接口测试执行器已停止")

    def execute_case(self, case: Dict[str, Any]) -> ApiCaseResult:
        """
        执行单个接口测试用例

        Args:
            case: 用例配置字典，包含:
                - id: 用例 ID
                - name: 用例名称
                - steps: 接口请求配置（JSON）

        Returns:
            ApiCaseResult: 执行结果
        """
        case_id = case.get("id", "")
        case_name = case.get("name", "")
        start = time.time()

        result = ApiCaseResult(
            case_id=case_id,
            case_name=case_name,
            status="pending",
        )

        try:
            # 解析请求配置
            steps_config = case.get("steps", {})
            if isinstance(steps_config, str):
                steps_config = json.loads(steps_config)

            method = str(steps_config.get("method", "GET")).upper()
            url_path = str(steps_config.get("url", "/"))
            headers = steps_config.get("headers") or {}
            body = steps_config.get("body")
            auth_required = steps_config.get("auth_required", True)
            sign_request = steps_config.get("sign_request", False)
            account_key = steps_config.get("account_key", "superadmin")
            assertions = steps_config.get("assertions") or []
            expected_desc = steps_config.get("description", "")

            # 模板变量替换（复用 TemplateResolver）
            resolver = TemplateResolver()
            resolved_headers = resolver.resolve(headers) if headers else {}
            resolved_body = resolver.resolve(body) if body is not None else None
            resolved_url = resolver.resolve(url_path) if isinstance(url_path, str) else url_path

            # 构造请求
            step = ApiStepResult(
                order=1,
                name=case_name,
                method=method,
                url=resolved_url,
                request_headers=dict(resolved_headers),
                request_body=resolved_body,
            )

            # 获取 Token
            token = None
            if auth_required:
                token = self.client.get_token(account_key)

            # 获取签名
            sign_headers = {}
            if sign_request:
                bearer_str = f"Bearer {token}" if token else ""
                body_for_sign = resolved_body if method in ("POST", "PUT", "PATCH") else None
                sign_headers = self.client._generate_sign(body_data=body_for_sign, bearer_token=bearer_str)

            # 合并请求头
            all_headers = {
                'Content-Type': 'application/json',
                'Accept': 'application/json, text/plain, */*',
            }
            if token:
                all_headers['Authorization'] = f"Bearer {token}"
            all_headers.update(sign_headers)
            all_headers.update(resolved_headers)

            step.request_headers = dict(all_headers)

            # 发送请求
            base_url = self.config.get("target", {}).get("base_url", "")
            full_url = resolved_url if resolved_url.startswith("http") else f"{base_url}{resolved_url}"
            step.url = full_url

            resp = self.client.session.request(
                method, full_url,
                json=resolved_body if method in ("POST", "PUT", "PATCH") else None,
                params=resolved_body if method in ("GET", "DELETE") and isinstance(resolved_body, dict) else None,
                headers=all_headers,
                verify=self.config.get("target", {}).get("verify_ssl", False),
                timeout=self.config.get("target", {}).get("timeout", 30),
            )

            elapsed_ms = resp.elapsed.total_seconds() * 1000
            step.status_code = resp.status_code
            step.response_time = elapsed_ms
            step.response_headers = {k: v for k, v in resp.headers.items()}

            # 响应体
            resp_text = resp.text
            if len(resp_text) > 100000:
                resp_text = resp_text[:100000] + "\n... (truncated)"
            step.response_body = resp_text

            # 执行断言
            step.assertions = AssertionEngine.validate(assertions, step)
            step.passed = AssertionEngine.is_passed(step.assertions)

            # 设置结果状态
            if step.passed:
                result.status = "passed"
            else:
                result.status = "failed"
                failed_parts = [a.message for a in step.assertions if not a.passed]
                result.error = "; ".join(failed_parts) if failed_parts else "断言未通过"

            result.step = step

        except Exception as e:
            logger.error(f"接口测试用例执行异常 [{case_name}]: {e}")
            import traceback
            logger.error(traceback.format_exc())
            result.status = "failed"
            result.error = str(e)
            result.step = ApiStepResult(
                order=1,
                name=case_name,
                method=case.get("steps", {}).get("method", "GET") if isinstance(case.get("steps"), dict) else "GET",
                url=case.get("steps", {}).get("url", "") if isinstance(case.get("steps"), dict) else "",
                error=str(e),
            )

        result.duration = time.time() - start
        return result

    def execute_cases(self, cases: List[Dict[str, Any]]) -> List[ApiCaseResult]:
        """
        批量执行接口测试用例

        Args:
            cases: 用例配置字典列表

        Returns:
            list[ApiCaseResult]: 执行结果列表
        """
        results: List[ApiCaseResult] = []
        for case in cases:
            if not self._is_running:
                logger.warning("执行器已停止，中止剩余用例")
                break
            result = self.execute_case(case)
            results.append(result)
            logger.info(
                f"用例 [{result.case_name}] 执行完成: {result.status}"
                + (f" ({result.error})" if result.error else "")
            )
        return results