#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：create_semantic_meeting_case.py
模块描述：创建「新建会议-语义化重写示范」用例（阶段 6.3 示范）

复杂用例通用执行机制（语义化用例执行）的落地示范：
- 原「新建会议-czj录入」21 步用例依赖 element_mapping_service 30+ 条模糊评分补丁，
  每次新用例都要走"实测 DOM → 加补丁 → 部署验证"循环。
- 本用例把复杂步骤（填名称/选会议室/切预定会议/议题/参会人/确定创建/查看详情/
  取消会议）全部改写为 semantic 语义目标 + verify 状态验证：
    semantic: {type, text, scope, container} —— 执行期经 映射表v2 精确键 lookup_by_key
              命中，动态数据（参会人/会议名称）由 {__CTX:...__} 上下文变量携带走
              强结构模式选择器，全程不依赖 USE_FUZZY_MAPPING 评分路径。
    verify:   防假通过的状态验证（text_visible / element_visible / dialog）
    finally:  api_call 数据自清理（查询同名会议断言无残留）

验收（PRD 八、阶段 6）：
- 新旧两版「新建会议」用例都通过
- 新版不依赖 USE_FUZZY_MAPPING
- 连跑两次无残留会议

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

import asyncio
import sys
import io

import httpx

BASE_URL = "http://localhost:8001"

# 被测系统信息（与登录模板一致）
TARGET_URL = "https://192.168.5.44"

# 会议管理模块（本地 DB 已验证存在）
MEETING_MODULE_ID = "module_e80a5d08b48d4c67a16cb86156320f25"

# 用例名称（幂等重跑：同名先删后建）
CASE_NAME = "新建会议-语义化重写示范（semantic-demo）"

DEFAULT_CONFIG = {
    "timeout": 60000,
    "screenshot": True,
    "auto_login": True,  # 走登录模板（含协议勾选），不再需要显式登录步骤
}

# 用例级参数：动态数据全部经 {__CTX:...__} 上下文引用，保证 创建/断言/清理 同名
CASE_PARAMETERS = {
    "meetingName": "自动化-{__RANDOM_NAME_8__}",
    "room": "北京展厅会议室",
    "participant": "admin@xty12",
}


def build_steps() -> list:
    """构造语义化步骤列表（对照原 21 步流程，登录步骤 1-6 由 auto_login 替代）"""
    steps = [
        # ============ 导航（稳定选择器，非语义重点） ============
        {"order": 1, "name": "点击【功能中心】展开", "action": "click",
         "params": {"selectors": [".home_nav_left"]}},
        {"order": 2, "name": "点击【会议预约】分类", "action": "click",
         "params": {"selectors": ["#reserve_enable"]}},
        {"order": 3, "name": "点击【新建会议】按钮", "action": "click",
         "params": {"selectors": ['[data-key="reserve_list.create"]']}},
        {"order": 4, "name": "等待会议表单加载", "action": "wait",
         "params": {"selector": "input[placeholder*='会议名称']", "timeout": 10000}},

        # ============ 原步骤 10-17（语义化改写核心） ============
        # 10 填会议名称：显式键 lookup_by_key(创建会议, 会议名称) → .meeting_list .meeting_name .el-input__inner
        {"order": 5, "name": "填写会议名称", "action": "fill",
         "params": {"value": "{__CTX:meetingName__}"},
         "semantic": {"type": "input", "text": "会议名称", "scope": "create_meeting"}},

        # 11 会议室选择：动态数据不写映射表，text 走 row_checkbox 模式选择器
        # （刻意不用"会议室选择"键——它映射到裸 .el-checkbox，会话15 的假通过根因）
        {"order": 6, "name": "勾选会议室：{__CTX:room__}", "action": "click",
         "params": {"force": True},
         "semantic": {"type": "row_checkbox", "text": "{__CTX:room__}",
                      "contains": "{__CTX:room__}", "scope": "create_meeting",
                      "container": {"type": "table", "selector": ".el-table"}}},

        # 12 时间切换为：预定会议 → 显式键 .time_btn > div:last-child .btns
        #    verify 用 text_visible（解析链首选择器兜底）防假通过
        {"order": 7, "name": "时间切换为：预定会议", "action": "click",
         "params": {},
         "semantic": {"type": "tab", "text": "预定会议", "scope": "create_meeting"},
         "verify": {"type": "text_visible", "text": "预定会议"}},

        # 13 会议议题输入 → 显式键 .topic_item input[placeholder='会议议题']
        {"order": 8, "name": "输入会议议题", "action": "fill",
         "params": {"value": "{__CTX:meetingName__}的议题"},
         "semantic": {"type": "input", "text": "议题输入框", "scope": "create_meeting"}},

        # 14 编辑按钮 → 显式键 .room .delimg img:nth-of-type(2)
        #    verify element_visible 确认参会人配置区出现（.room .user_list）
        {"order": 9, "name": "点击左侧会议室框的编辑按钮", "action": "click",
         "params": {},
         "semantic": {"type": "icon", "text": "编辑会议室", "scope": "create_meeting",
                      "container": {"type": "card", "selector": ".room"}},
         "verify": {"type": "element_visible", "selector": ".room .user_list"}},

        # 15 勾选参会人：动态数据（admin@xty12）走 row_checkbox 模式选择器
        {"order": 10, "name": "勾选参会人：{__CTX:participant__}", "action": "click",
         "params": {"force": True},
         "semantic": {"type": "row_checkbox", "text": "{__CTX:participant__}",
                      "contains": "{__CTX:participant__}", "scope": "create_meeting",
                      "container": {"type": "table", "selector": ".el-table"}}},

        # 16 编辑完成按钮 → 显式键 .room .user_list button:has-text("完成")
        {"order": 11, "name": "点击左侧会议室框的【完成】按钮", "action": "click",
         "params": {},
         "semantic": {"type": "button", "text": "编辑完成按钮", "scope": "create_meeting",
                      "container": {"type": "card", "selector": ".room"}}},

        # 17 确定创建 → 显式键 .btn button.el-button + verify 弹窗"创建成功"
        {"order": 12, "name": "点击【确定创建】按钮", "action": "click",
         "params": {},
         "semantic": {"type": "button", "text": "确定创建", "scope": "create_meeting"},
         "verify": {"type": "dialog", "text": "创建成功"}},

        # 18 查看详情（创建成功弹窗）→ 显式键 创建会议-查看详情（.el-message-box）
        {"order": 13, "name": "点击弹窗的【查看详情】按钮", "action": "click",
         "params": {},
         "semantic": {"type": "button", "text": "查看详情", "scope": "create_meeting"}},

        # 19 等待会议详情页（.buttom_btn 底部操作区出现）
        {"order": 14, "name": "等待会议详情页", "action": "wait",
         "params": {"selector": ".buttom_btn", "timeout": 10000}},

        # 20 取消会议 → 显式键 .buttom_btn .el-button--info.is-plain
        {"order": 15, "name": "点击底部的【取消会议】按钮", "action": "click",
         "params": {},
         "semantic": {"type": "button", "text": "取消会议", "scope": "meeting_detail",
                      "container": {"type": "footer", "selector": ".buttom_btn"}}},

        # 21 二次确认确定 → 显式键 .el-dialog:visible .el-dialog__footer .el-button--primary
        #    verify 弹窗"取消成功"（非强制，仅告警——文案以真实系统为准）
        {"order": 16, "name": "点击二次确认弹窗的【确定】按钮", "action": "click",
         "params": {},
         "semantic": {"type": "button", "text": "取消确认确定", "scope": "meeting_detail",
                      "container": {"type": "dialog"}},
         "verify": {"type": "dialog", "text": "取消成功"}},

        # 22 finally 数据自清理：查询同名会议断言无残留（api_call，不依赖浏览器）
        # 真实端点（2026-08-18 抓包确认）：GET /meetingV3/api/message/getMessagePageList
        #   - 5.44 meetingV3 会议列表首屏即该 HTTP 接口（实时刷新走 MQTT 但首屏走 HTTP）
        #   - 需 authorization 头（无 token 返回 401 令牌不能为空）；_do_api_call 已实现
        #     自动从浏览器会话 localStorage.meet_token 注入，无需手动维护 token
        #   - body（GET→query params）带 search 会议名；断言 body_not_contains 校验
        #     响应体不含该会议名（无残留）。finally 失败会将主流程 passed 翻为 failed。
        {"order": 17, "name": "清理残留会议（同名断言无残留）", "action": "api_call",
         "run_on": "finally",
         "params": {
             "api_case": {
                 "name": "查询同名会议",
                 "steps": {
                     "method": "GET",
                     "url": "/meetingV3/api/message/getMessagePageList",
                     "body": {"companyNumber": "CN-WQF-UBAINS", "queryType": 0,
                              "pageNum": 1, "pageSize": 100,
                              "search": "{__CTX:meetingName__}"},
                     "assertions": [
                         {"type": "body_not_contains", "value": "{__CTX:meetingName__}"},
                     ],
                 },
             },
         }},
    ]
    return steps


async def delete_existing(client: httpx.AsyncClient) -> None:
    """幂等：删除同名旧用例（先查后删）"""
    r = await client.get(f"{BASE_URL}/api/cases", params={"name": CASE_NAME})
    if r.status_code == 200:
        items = r.json().get("items", r.json()) if isinstance(r.json(), dict) else r.json()
        for item in (items or []):
            cid = item.get("id")
            if cid and item.get("name") == CASE_NAME:
                dr = await client.delete(f"{BASE_URL}/api/cases/{cid}")
                print(f"  已删除旧用例: {cid} (HTTP {dr.status_code})")


async def main() -> None:
    payload = {
        "module_id": MEETING_MODULE_ID,
        "name": CASE_NAME,
        "description": (
            "语义化重写示范：复杂步骤全部走 semantic 语义目标 + verify 状态验证 + "
            "finally api_call 自清理，不依赖 USE_FUZZY_MAPPING 模糊评分。"
        ),
        "priority": "high",
        "tags": ["语义化", "新建会议", "阶段6示范"],
        "steps": build_steps(),
        "parameters": CASE_PARAMETERS,
        "config": DEFAULT_CONFIG,
    }

    async with httpx.AsyncClient(timeout=30) as client:
        await delete_existing(client)
        r = await client.post(f"{BASE_URL}/api/cases", json=payload)
        if r.status_code == 201:
            data = r.json()
            print("✅ 用例创建成功")
            print(f"   ID: {data.get('id')}")
            print(f"   名称: {data.get('name')}")
            print(f"   步骤数: {len(payload['steps'])}")
            print(f"   参数: {CASE_PARAMETERS}")
            print("\n   semantic 步骤明细：")
            for s in payload["steps"]:
                tag = []
                if s.get("semantic"):
                    tag.append(f"semantic[{s['semantic'].get('type')}:{s['semantic'].get('text')}]")
                if s.get("verify"):
                    tag.append(f"verify[{s['verify'].get('type')}]")
                if s.get("run_on"):
                    tag.append(f"run_on:{s['run_on']}")
                print(f"     {s['order']:>2}. {s['name']}  {' '.join(tag)}")
        else:
            print(f"❌ 用例创建失败: HTTP {r.status_code}")
            print(r.text[:1000])


if __name__ == "__main__":
    # 标准输出编码（Windows GBK 控制台兼容）——仅在直接运行时重定向，
    # import 本模块（如语义链单测）不应污染宿主 stdout。
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
    asyncio.run(main())
