# -*- coding: utf-8 -*-
"""
认证系统属性测试全量运行脚本（100 次迭代）

背景：
  Spec: 03-miniapp-auth-system（小程序用户认证系统）
  任务 11: 属性测试全量运行
  前面各任务中的属性测试仅用 5 次迭代快速验证逻辑正确性，
  本脚本集中对所有 15 个属性测试执行 100 次迭代，确保健壮性。

系统概述：
  NeoZQYY 台球门店全栈数据平台的小程序认证系统，涵盖：
  - 微信登录（code2Session → openid → JWT）
  - 用户申请审核（site_code 映射、人员匹配、角色分配）
  - RBAC 权限控制（多店铺隔离、权限中间件）
  - JWT 令牌管理（受限令牌、店铺切换、过期拒绝）

数据库：
  测试库 test_zqyy_app（通过 APP_DB_DSN 环境变量连接）
  禁止连接正式库 zqyy_app

运行方式：
  cd C:\\NeoZQYY
  python scripts/ops/_run_auth_pbt_full.py [--concurrency N] [--only P1,P2,...] [--skip P3,P5]

设计要求：
  1. 后台逐个运行每个属性测试（subprocess），前台定时监控
  2. 控制数据库并发：同一时刻只运行 1 个测试（默认），避免占满连接
  3. 每完成一个测试输出进度
  4. 全部完成后生成详细 MD 格式报告
"""
import os
import sys
import subprocess
import time
import json
import argparse
from datetime import datetime, timezone, timedelta
from pathlib import Path

# ── 环境初始化 ──────────────────────────────────────────────────
from dotenv import load_dotenv

PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
load_dotenv(PROJECT_ROOT / ".env")

APP_DB_DSN = os.environ.get("APP_DB_DSN")
if not APP_DB_DSN:
    print("❌ APP_DB_DSN 环境变量未设置，无法运行属性测试。")
    sys.exit(1)

# ── 测试定义 ──────────────────────────────────────────────────
# 每个属性测试的 pytest 节点 ID 和描述
PROPERTY_TESTS = [
    {
        "id": "P1",
        "name": "迁移脚本幂等性",
        "class": "TestProperty1MigrationIdempotency",
        "validates": "Requirements 1.9, 2.4, 11.5",
        "description": "对认证系统迁移脚本（DDL + 种子数据）连续执行两次，验证幂等性",
        "db_write": True,  # 标记是否写数据库
    },
    {
        "id": "P2",
        "name": "登录创建/查找用户",
        "class": "TestProperty2LoginCreateFindUser",
        "validates": "Requirements 3.2, 3.3",
        "description": "随机 openid 登录，验证新用户创建（pending）和已有用户查找",
        "db_write": True,
    },
    {
        "id": "P3",
        "name": "disabled 用户登录拒绝",
        "class": "TestProperty3DisabledUserLoginRejection",
        "validates": "Requirements 3.5",
        "description": "disabled 状态用户登录返回 403，不签发 JWT",
        "db_write": True,
    },
    {
        "id": "P4",
        "name": "申请创建正确性",
        "class": "TestProperty4ApplicationCreation",
        "validates": "Requirements 4.1, 4.2, 4.3, 4.4",
        "description": "随机合法申请数据，验证申请记录创建和 site_code 映射",
        "db_write": True,
    },
    {
        "id": "P5",
        "name": "手机号格式验证",
        "class": "TestProperty5PhoneFormatValidation",
        "validates": "Requirements 4.5",
        "description": "随机非法手机号提交申请，验证 422 拒绝",
        "db_write": False,
    },
    {
        "id": "P6",
        "name": "重复申请拒绝",
        "class": "TestProperty6DuplicateApplicationRejection",
        "validates": "Requirements 4.6",
        "description": "已有 pending 申请的用户再次提交，验证 409 拒绝",
        "db_write": True,
    },
    {
        "id": "P7",
        "name": "人员匹配合并正确性",
        "class": "TestProperty7MatchingMerge",
        "validates": "Requirements 5.1, 5.2, 5.3, 5.4",
        "description": "随机 site_id + phone 组合，验证助教/员工匹配结果合并",
        "db_write": False,
    },
    {
        "id": "P8",
        "name": "审核操作正确性",
        "class": "TestProperty8ReviewOperations",
        "validates": "Requirements 6.1, 6.2, 6.3, 6.4, 6.5",
        "description": "随机 pending 申请执行批准/拒绝，验证状态流转和绑定创建",
        "db_write": True,
    },
    {
        "id": "P9",
        "name": "非 pending 审核拒绝",
        "class": "TestProperty9NonPendingReviewRejection",
        "validates": "Requirements 6.6",
        "description": "非 pending 状态申请执行审核，验证 409 拒绝",
        "db_write": True,
    },
    {
        "id": "P10",
        "name": "用户状态查询完整性",
        "class": "TestProperty10UserStatusQueryCompleteness",
        "validates": "Requirements 7.1, 7.2",
        "description": "随机用户状态组合，验证查询返回完整的申请列表和店铺信息",
        "db_write": True,
    },
    {
        "id": "P11",
        "name": "多店铺角色独立分配",
        "class": "TestProperty11MultiSiteRoleIndependence",
        "validates": "Requirements 8.1",
        "description": "随机用户 + 多 site_id，验证角色独立分配互不干扰",
        "db_write": True,
    },
    {
        "id": "P12",
        "name": "店铺切换令牌正确性",
        "class": "TestProperty12SiteSwitchTokenCorrectness",
        "validates": "Requirements 8.2, 10.4",
        "description": "多店铺用户切换店铺，验证新 JWT 中 site_id 和 roles 正确",
        "db_write": True,
    },
    {
        "id": "P13",
        "name": "权限中间件拦截正确性",
        "class": "TestProperty13PermissionMiddleware",
        "validates": "Requirements 8.3, 9.1, 9.2, 9.3",
        "description": "随机用户 + 权限组合，验证中间件拦截/放行逻辑",
        "db_write": True,
    },
    {
        "id": "P14",
        "name": "JWT payload 结构一致性",
        "class": "TestProperty14JwtPayloadStructure",
        "validates": "Requirements 10.1, 10.2, 10.3",
        "description": "随机用户状态签发 JWT，验证 payload 字段与状态一致",
        "db_write": False,
    },
    {
        "id": "P15",
        "name": "JWT 过期/无效拒绝",
        "class": "TestProperty15JwtExpiredInvalidRejection",
        "validates": "Requirements 9.4",
        "description": "随机过期/篡改/错密钥/垃圾 JWT，验证 401 拒绝",
        "db_write": False,
    },
]

TEST_FILE = "tests/test_auth_system_properties.py"
MAX_EXAMPLES = 100


# ── 时区 ──────────────────────────────────────────────────────
CST = timezone(timedelta(hours=8))


def _now_cst() -> datetime:
    return datetime.now(CST)


def _fmt(dt: datetime) -> str:
    return dt.strftime("%Y-%m-%d %H:%M:%S")


# ── 运行单个测试 ─────────────────────────────────────────────
def run_single_test(prop: dict, max_examples: int = MAX_EXAMPLES) -> dict:
    """
    运行单个属性测试，返回结果字典。
    通过 HYPOTHESIS_MAX_EXAMPLES 环境变量覆盖迭代次数。
    """
    node_id = f"{TEST_FILE}::{prop['class']}"
    start = _now_cst()
    print(f"\n{'='*60}")
    print(f"▶ [{prop['id']}] {prop['name']}  (max_examples={max_examples})")
    print(f"  节点: {node_id}")
    print(f"  开始: {_fmt(start)}")
    print(f"{'='*60}")

    env = os.environ.copy()
    # hypothesis 通过 settings profile 或环境变量控制
    # 我们用 --override-ini 传递 hypothesis settings
    cmd = [
        sys.executable, "-m", "pytest",
        node_id,
        "-v",
        "--tb=short",
        f"--hypothesis-seed=0",  # 固定种子保证可复现
        "-x",  # 遇到第一个失败就停止（节省时间）
        f"-o", f"hypothesis_settings_max_examples={max_examples}",
    ]

    # hypothesis 不支持 pytest -o 覆盖 max_examples，
    # 改用环境变量 + conftest 或直接 patch
    # 实际方案：通过 HYPOTHESIS_MAX_EXAMPLES 环境变量
    env["HYPOTHESIS_MAX_EXAMPLES"] = str(max_examples)

    result = subprocess.run(
        cmd,
        capture_output=True,
        text=True,
        cwd=str(PROJECT_ROOT),
        env=env,
        timeout=600,  # 单个测试最多 10 分钟
    )

    end = _now_cst()
    duration = (end - start).total_seconds()

    # 解析结果
    passed = result.returncode == 0
    stdout = result.stdout or ""
    stderr = result.stderr or ""

    # 提取测试计数
    test_count = _extract_test_count(stdout)

    outcome = {
        "id": prop["id"],
        "name": prop["name"],
        "class": prop["class"],
        "validates": prop["validates"],
        "description": prop["description"],
        "db_write": prop["db_write"],
        "passed": passed,
        "returncode": result.returncode,
        "duration_sec": round(duration, 1),
        "start_time": _fmt(start),
        "end_time": _fmt(end),
        "test_count": test_count,
        "stdout_tail": _tail(stdout, 30),
        "stderr_tail": _tail(stderr, 10),
    }

    # 实时进度输出
    status = "✅ PASSED" if passed else "❌ FAILED"
    print(f"\n  结果: {status}  耗时: {duration:.1f}s  测试数: {test_count}")
    if not passed:
        print(f"  --- stdout 尾部 ---")
        print(outcome["stdout_tail"])
        if stderr.strip():
            print(f"  --- stderr 尾部 ---")
            print(outcome["stderr_tail"])

    return outcome


def _extract_test_count(stdout: str) -> str:
    """从 pytest 输出中提取测试计数，如 '3 passed' 或 '2 passed, 1 failed'"""
    for line in reversed(stdout.splitlines()):
        line = line.strip()
        if "passed" in line or "failed" in line or "error" in line:
            # 去掉 ANSI 颜色码
            import re
            clean = re.sub(r'\x1b\[[0-9;]*m', '', line)
            if "=" in clean:
                return clean.split("=")[-1].strip().rstrip("=").strip()
    return "unknown"


def _tail(text: str, n: int) -> str:
    """取文本最后 n 行"""
    lines = text.splitlines()
    return "\n".join(lines[-n:]) if len(lines) > n else text


# ── 生成 MD 报告 ─────────────────────────────────────────────
def generate_report(results: list[dict], total_start: datetime, total_end: datetime) -> str:
    """生成详细的 MD 格式测试报告"""
    total_duration = (total_end - total_start).total_seconds()
    passed_count = sum(1 for r in results if r["passed"])
    failed_count = len(results) - passed_count
    all_passed = failed_count == 0

    lines = []
    lines.append(f"# 认证系统属性测试全量报告")
    lines.append(f"")
    lines.append(f"- Spec: `03-miniapp-auth-system`（小程序用户认证系统）")
    lines.append(f"- 任务: 11. 属性测试全量运行（{MAX_EXAMPLES} 次迭代）")
    lines.append(f"- 测试文件: `{TEST_FILE}`")
    lines.append(f"- 数据库: `test_zqyy_app`（通过 `APP_DB_DSN`）")
    lines.append(f"- 运行时间: {_fmt(total_start)} → {_fmt(total_end)}（共 {total_duration:.0f}s）")
    lines.append(f"- 总体结果: {'✅ 全部通过' if all_passed else f'❌ {failed_count} 个失败'}")
    lines.append(f"- 通过/总数: {passed_count}/{len(results)}")
    lines.append(f"")

    # 汇总表
    lines.append(f"## 汇总")
    lines.append(f"")
    lines.append(f"| # | 属性 | 验证需求 | 结果 | 耗时 | 测试数 | 写库 |")
    lines.append(f"|---|------|---------|------|------|--------|------|")
    for r in results:
        status = "✅" if r["passed"] else "❌"
        db = "是" if r["db_write"] else "否"
        lines.append(
            f"| {r['id']} | {r['name']} | {r['validates']} | "
            f"{status} | {r['duration_sec']}s | {r['test_count']} | {db} |"
        )
    lines.append(f"")

    # 失败详情
    failed = [r for r in results if not r["passed"]]
    if failed:
        lines.append(f"## 失败详情")
        lines.append(f"")
        for r in failed:
            lines.append(f"### {r['id']} {r['name']}")
            lines.append(f"")
            lines.append(f"- 验证需求: {r['validates']}")
            lines.append(f"- 描述: {r['description']}")
            lines.append(f"- 返回码: {r['returncode']}")
            lines.append(f"- 耗时: {r['duration_sec']}s")
            lines.append(f"")
            lines.append(f"```")
            lines.append(r["stdout_tail"])
            lines.append(f"```")
            if r["stderr_tail"].strip():
                lines.append(f"")
                lines.append(f"stderr:")
                lines.append(f"```")
                lines.append(r["stderr_tail"])
                lines.append(f"```")
            lines.append(f"")

    # 各测试详情
    lines.append(f"## 各属性测试详情")
    lines.append(f"")
    for r in results:
        status = "✅ PASSED" if r["passed"] else "❌ FAILED"
        lines.append(f"### {r['id']} {r['name']} — {status}")
        lines.append(f"")
        lines.append(f"- 描述: {r['description']}")
        lines.append(f"- 验证需求: {r['validates']}")
        lines.append(f"- 测试类: `{r['class']}`")
        lines.append(f"- 开始: {r['start_time']}  结束: {r['end_time']}")
        lines.append(f"- 耗时: {r['duration_sec']}s")
        lines.append(f"- 测试数: {r['test_count']}")
        lines.append(f"- 写库: {'是' if r['db_write'] else '否'}")
        lines.append(f"")

    # 数据库带宽说明
    lines.append(f"## 数据库资源控制")
    lines.append(f"")
    lines.append(f"- 串行执行：同一时刻仅运行 1 个属性测试，避免数据库连接争用")
    lines.append(f"- 写库测试（{sum(1 for r in results if r['db_write'])} 个）每个测试内部自行清理测试数据")
    lines.append(f"- 纯读/内存测试（{sum(1 for r in results if not r['db_write'])} 个）不产生数据库写入")
    lines.append(f"- 测试间无并发，为其他调试任务保留数据库带宽")
    lines.append(f"")

    return "\n".join(lines)


# ── conftest 补丁 ─────────────────────────────────────────────
CONFTEST_PATCH = '''# -*- coding: utf-8 -*-
"""
临时 conftest：通过环境变量覆盖 hypothesis max_examples。
由 _run_auth_pbt_full.py 自动生成，测试完成后自动删除。

原理：在 pytest 收集完测试后，遍历所有 hypothesis 测试项，
替换其 settings 中的 max_examples 为环境变量指定的值。
"""
import os

_max_env = os.environ.get("HYPOTHESIS_MAX_EXAMPLES")


def pytest_collection_modifyitems(items):
    """收集完测试后，覆盖 hypothesis settings 中的 max_examples"""
    if not _max_env:
        return
    forced_max = int(_max_env)
    from hypothesis import settings as _settings

    for item in items:
        # hypothesis 测试会在 item 上挂 hypothesis_settings
        if hasattr(item, "_hypothesis_internal_use_settings"):
            old = item._hypothesis_internal_use_settings
            item._hypothesis_internal_use_settings = _settings(
                old,
                max_examples=forced_max,
            )
'''


def _ensure_conftest():
    """确保 tests/conftest.py 中有 hypothesis max_examples 覆盖逻辑"""
    conftest_path = PROJECT_ROOT / "tests" / "conftest.py"
    marker = "HYPOTHESIS_MAX_EXAMPLES"

    if conftest_path.exists():
        content = conftest_path.read_text(encoding="utf-8")
        if marker in content:
            return False  # 已有，不需要补丁
        # 追加
        with open(conftest_path, "a", encoding="utf-8") as f:
            f.write("\n\n" + CONFTEST_PATCH)
        return True
    else:
        conftest_path.write_text(CONFTEST_PATCH, encoding="utf-8")
        return True


def _cleanup_conftest():
    """清理临时 conftest 补丁"""
    conftest_path = PROJECT_ROOT / "tests" / "conftest.py"
    if not conftest_path.exists():
        return
    content = conftest_path.read_text(encoding="utf-8")
    if "由 _run_auth_pbt_full.py 自动生成" in content:
        # 如果整个文件都是我们生成的，删除
        if content.strip().startswith("# -*- coding: utf-8 -*-\n") and "HYPOTHESIS_MAX_EXAMPLES" in content:
            lines = content.split("HYPOTHESIS_MAX_EXAMPLES")
            # 简单判断：如果文件很短且主要是我们的内容
            if len(content) < 500:
                conftest_path.unlink()
                return
        # 否则只移除我们追加的部分
        marker = "\n\n# -*- coding: utf-8 -*-\n\"\"\"\n临时 conftest"
        if marker in content:
            content = content[:content.index(marker)]
            conftest_path.write_text(content, encoding="utf-8")


# ── 主流程 ────────────────────────────────────────────────────
def main():
    parser = argparse.ArgumentParser(description="认证系统属性测试全量运行")
    parser.add_argument(
        "--max-examples", type=int, default=MAX_EXAMPLES,
        help=f"每个属性测试的迭代次数（默认 {MAX_EXAMPLES}）",
    )
    parser.add_argument(
        "--only", type=str, default=None,
        help="仅运行指定属性（逗号分隔，如 P1,P2,P14）",
    )
    parser.add_argument(
        "--skip", type=str, default=None,
        help="跳过指定属性（逗号分隔，如 P3,P5）",
    )
    parser.add_argument(
        "--report-dir", type=str, default=None,
        help="报告输出目录（默认 export/ 下）",
    )
    args = parser.parse_args()

    # 筛选测试
    tests_to_run = PROPERTY_TESTS[:]
    if args.only:
        only_ids = {x.strip().upper() for x in args.only.split(",")}
        tests_to_run = [t for t in tests_to_run if t["id"] in only_ids]
    if args.skip:
        skip_ids = {x.strip().upper() for x in args.skip.split(",")}
        tests_to_run = [t for t in tests_to_run if t["id"] not in skip_ids]

    if not tests_to_run:
        print("❌ 没有要运行的测试。")
        sys.exit(1)

    max_ex = args.max_examples

    print(f"╔{'═'*58}╗")
    print(f"║  认证系统属性测试全量运行                                ║")
    print(f"║  迭代次数: {max_ex:<5}  测试数: {len(tests_to_run):<3}                        ║")
    print(f"║  数据库: test_zqyy_app（串行执行，控制带宽）            ║")
    print(f"╚{'═'*58}╝")

    # 确保 conftest 补丁
    patched = _ensure_conftest()
    if patched:
        print("📝 已注入 conftest.py hypothesis max_examples 覆盖")

    total_start = _now_cst()
    results = []

    try:
        for i, prop in enumerate(tests_to_run, 1):
            print(f"\n📊 进度: {i}/{len(tests_to_run)}")
            outcome = run_single_test(prop, max_examples=max_ex)
            results.append(outcome)

            # 测试间短暂休息，释放数据库连接
            if i < len(tests_to_run):
                time.sleep(1)

    except KeyboardInterrupt:
        print("\n\n⚠️ 用户中断，生成已完成部分的报告...")
    except Exception as e:
        print(f"\n\n❌ 运行异常: {e}")
    finally:
        total_end = _now_cst()

        # 生成报告
        if results:
            report = generate_report(results, total_start, total_end)

            # 确定报告路径
            report_dir = Path(args.report_dir) if args.report_dir else PROJECT_ROOT / "export" / "reports"
            report_dir.mkdir(parents=True, exist_ok=True)
            timestamp = total_start.strftime("%Y%m%d_%H%M%S")
            report_path = report_dir / f"auth_pbt_full_{timestamp}.md"
            report_path.write_text(report, encoding="utf-8")

            passed = sum(1 for r in results if r["passed"])
            failed = len(results) - passed
            print(f"\n{'='*60}")
            print(f"📋 报告已生成: {report_path}")
            print(f"   通过: {passed}  失败: {failed}  总计: {len(results)}")
            print(f"   总耗时: {(total_end - total_start).total_seconds():.0f}s")
            print(f"{'='*60}")

        # 清理 conftest 补丁
        if patched:
            _cleanup_conftest()
            print("🧹 已清理临时 conftest 补丁")


if __name__ == "__main__":
    main()
