#!/usr/bin/env python3
"""Create/update the persistent subset of the S12 school system Apifox scenario."""

from __future__ import annotations

import copy
import importlib.util
from pathlib import Path


BASE_SCRIPT = Path(
    "/Users/guodongwei/Documents/zhct/zhctprompt/"
    "work_food_safety/2026-09-24-apifox-full-api-automation/"
    "apifox/scripts/sync_school_s00_s06_scenarios.py"
)
SPEC = importlib.util.spec_from_file_location("school_s00_s06_sync", BASE_SCRIPT)
if SPEC is None or SPEC.loader is None:
    raise RuntimeError(f"Cannot load shared Apifox helpers from {BASE_SCRIPT}")
shared = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(shared)


NAME = "S12-用户角色与审计（双库真实写入）"
PREFIX = "APIFOX_S12"
DESCRIPTION = (
    "学校端 S12 系统管理完整主流程：公告及附件、食堂配置、权限读取、角色及权限新增/编辑/回查、"
    "学校账号新增/编辑/启停/回查、机构类型持久化和审计列表/详情/CSV。"
    "正向数据使用真实中文业务语义，批次号只作档案追踪。DBCOMPAT-0021 修复后，"
    "公告、附件和食堂配置必须再通过双库原表与重启读回才能关闭缺陷。测试数据保留用于原表取证。"
)


OPERATIONS = [
    {
        "name": "新增2026年秋季学校食品安全培训公告",
        "method": "POST",
        "path": "/school/system/announcements",
        "body": {
            "title": "2026年秋季学校食品安全培训通知（档案{{business_batch}}）",
            "content": "请各食堂负责人于本周五前完成秋季食品安全培训，并提交签到记录与现场照片。",
        },
        "extract": ("announcement_id", "$['data']['id']"),
        "tests": [
            ("公告新增：返回稳定 UUID 格式 ID", "pm.expect(__json.data.id).to.match(/^AN-[0-9a-f-]{36}$/)"),
            ("公告新增：中文标题和正文原样返回", "pm.expect(__json.data.title).to.equal('2026年秋季学校食品安全培训通知（档案'+pm.variables.get('business_batch')+'）'); pm.expect(__json.data.content).to.equal('请各食堂负责人于本周五前完成秋季食品安全培训，并提交签到记录与现场照片。')"),
        ],
    },
    {
        "name": "为本轮公告登记培训签到表附件元数据",
        "method": "POST",
        "path": "/school/system/announcements/attach",
        "body": {
            "id": "{{announcement_id}}",
            "schoolId": 1,
            "name": "2026年秋季食品安全培训签到表（电子回执）.pdf",
            "url": "/files/announcements/{{announcement_id}}/training-signin-2026-autumn.pdf",
        },
        "extract": ("announcement_attachment_id", "$['data']['id']"),
        "tests": [
            ("公告附件：返回稳定 UUID 格式 ID", "pm.expect(__json.data.id).to.match(/^ATT-[0-9a-f-]{36}$/)"),
            ("公告附件：中文名称和本地元数据地址正确", "pm.expect(__json.data.name).to.equal('2026年秋季食品安全培训签到表（电子回执）.pdf'); pm.expect(__json.data.url).to.equal('/files/announcements/'+pm.variables.get('announcement_id')+'/training-signin-2026-autumn.pdf')"),
        ],
    },
    {
        "name": "从公告列表回查本轮公告与附件",
        "method": "GET",
        "path": "/school/system/announcements?page=1&pageSize=100&schoolId=1",
        "tests": [
            ("公告列表：包含本轮公告", "const row=__json.data.items.find(function(x){return x.id===pm.variables.get('announcement_id');}); pm.expect(row).to.exist; pm.expect(row.title).to.include(pm.variables.get('business_batch'))"),
            ("公告列表：附件元数据已持久化", "const row=__json.data.items.find(function(x){return x.id===pm.variables.get('announcement_id');}); pm.expect(row.attachments).to.have.lengthOf(1); pm.expect(row.attachments[0].id).to.equal(pm.variables.get('announcement_attachment_id'))"),
        ],
    },
    {
        "name": "从公告详情回查中文正文与附件",
        "method": "GET",
        "path": "/school/system/announcements/detail?id={{announcement_id}}&schoolId=1",
        "tests": [
            ("公告详情：学校范围与中文正文正确", "pm.expect(Number(__json.data.schoolId)).to.equal(1); pm.expect(__json.data.content).to.equal('请各食堂负责人于本周五前完成秋季食品安全培训，并提交签到记录与现场照片。')"),
            ("公告详情：附件 ID、名称与 URL 完整", "pm.expect(__json.data.attachments).to.have.lengthOf(1); const a=__json.data.attachments[0]; pm.expect(a.id).to.equal(pm.variables.get('announcement_attachment_id')); pm.expect(a.name).to.include('食品安全培训签到表'); pm.expect(a.url).to.include(pm.variables.get('announcement_id'))"),
        ],
    },
    {
        "name": "保存康源实验学校第一食堂基本配置",
        "method": "POST",
        "path": "/school/system/canteen",
        "body": {
            "name": "康源实验学校第一食堂",
            "manager": "食品安全总监王建国",
            "phone": "13601002030",
            "address": "北京市海淀区书院路18号综合楼一层",
        },
        "tests": [
            ("食堂配置：中文业务字段原样返回", "pm.expect(__json.data.name).to.equal('康源实验学校第一食堂'); pm.expect(__json.data.manager).to.equal('食品安全总监王建国'); pm.expect(__json.data.phone).to.equal('13601002030'); pm.expect(__json.data.address).to.equal('北京市海淀区书院路18号综合楼一层')"),
            ("食堂配置：更新时间已返回", "pm.expect(__json.data.updatedAt).to.be.a('string').and.not.empty"),
        ],
    },
    {
        "name": "回查康源实验学校第一食堂配置",
        "method": "GET",
        "path": "/school/system/canteen",
        "tests": [
            ("食堂配置回查：名称、负责人、电话和地址完整", "pm.expect(__json.data.name).to.equal('康源实验学校第一食堂'); pm.expect(__json.data.manager).to.equal('食品安全总监王建国'); pm.expect(__json.data.phone).to.equal('13601002030'); pm.expect(__json.data.address).to.equal('北京市海淀区书院路18号综合楼一层')"),
        ],
    },
    {
        "name": "读取系统权限树",
        "method": "GET",
        "path": "/school/system/permissions",
        "tests": [
            ("权限树：返回分组数组", "pm.expect(__json.data).to.be.an('array').and.not.empty"),
            ("权限树：包含用户与审计权限", "const keys=[]; __json.data.forEach(function(g){(g.children||[]).forEach(function(x){keys.push(x.key);});}); pm.expect(keys).to.include('system.users'); pm.expect(keys).to.include('system.audit')"),
        ],
    },
    {
        "name": "新增学校食品安全数据管理员角色",
        "method": "POST",
        "path": "/school/system/roles/save-with-permissions",
        "body": {
            "schoolId": 1,
            "name": "{{role_name}}",
            "remark": "负责学校食安台账、账号协同和审计查询。",
            "permissions": ["system.users", "system.roles", "system.audit", "audit.view"],
        },
        "extract": ("system_role_id", "$.data.id"),
        "tests": [("角色新增：返回正整数 ID", "pm.expect(Number(__json.data.id)).to.be.greaterThan(0)")],
    },
    {
        "name": "按中文名称回查角色",
        "method": "GET",
        "path": "/school/system/roles?schoolId=1&q={{role_name}}",
        "tests": [
            ("角色列表：包含本轮角色和中文备注", "const id=Number(pm.variables.get('system_role_id')); const row=__json.data.find(function(x){return Number(x.id)===id;}); pm.expect(row).to.exist; pm.expect(row.name).to.equal(pm.variables.get('role_name')); pm.expect(row.remark).to.equal('负责学校食安台账、账号协同和审计查询。')"),
        ],
    },
    {
        "name": "回查角色初始权限",
        "method": "GET",
        "path": "/school/system/roles/permissions?name={{role_name}}&schoolId=1",
        "tests": [
            ("角色权限：初始四项完整", "pm.expect(__json.data.name).to.equal(pm.variables.get('role_name')); const p=__json.data.permissions||[]; ['system.users','system.roles','system.audit','audit.view'].forEach(function(k){pm.expect(p).to.include(k);}); pm.expect(p.length).to.equal(4)"),
        ],
    },
    {
        "name": "更新角色权限加入公告只读",
        "method": "POST",
        "path": "/school/system/roles/permissions",
        "body": {
            "name": "{{role_name}}",
            "schoolId": 1,
            "permissions": ["system.users", "system.roles", "system.audit", "audit.view", "announcements.read"],
        },
        "tests": [("角色权限更新：ok=true", "pm.expect(__json.data.ok).to.equal(true)")],
    },
    {
        "name": "更新角色中文职责说明",
        "method": "PATCH",
        "path": "/school/system/roles",
        "body": {
            "id": "{{system_role_id}}",
            "patch": {"remark": "负责学校食安台账复核、账号协同与审计追踪。"},
        },
        "tests": [("角色编辑：ok=true", "pm.expect(__json.data.ok).to.equal(true)")],
    },
    {
        "name": "回查角色最终权限与职责",
        "method": "GET",
        "path": "/school/system/roles?schoolId=1&q={{role_name}}",
        "tests": [
            ("角色最终状态：备注已更新", "const id=Number(pm.variables.get('system_role_id')); const row=__json.data.find(function(x){return Number(x.id)===id;}); pm.expect(row).to.exist; pm.expect(row.remark).to.equal('负责学校食安台账复核、账号协同与审计追踪。')"),
        ],
    },
    {
        "name": "回查角色最终五项权限",
        "method": "GET",
        "path": "/school/system/roles/permissions?name={{role_name}}&schoolId=1",
        "tests": [
            ("角色最终权限：五项完整且无多余项", "const p=__json.data.permissions||[]; ['system.users','system.roles','system.audit','audit.view','announcements.read'].forEach(function(k){pm.expect(p).to.include(k);}); pm.expect(p.length).to.equal(5)"),
        ],
    },
    {
        "name": "新增康源实验学校食品安全专员账号",
        "method": "POST",
        "path": "/school/system/users",
        "body": {
            "schoolId": 1,
            "name": "刘明慧",
            "phone": "{{system_user_phone}}",
            "roles": ["{{role_name}}"],
            "remark": "康源实验学校食品安全专员，负责日常台账复核。",
            "enabled": True,
            "password": "{{system_user_password}}",
        },
        "extract": ("system_user_id", "$.data.id"),
        "tests": [("学校账号新增：返回正整数 ID", "pm.expect(Number(__json.data.id)).to.be.greaterThan(0)")],
    },
    {
        "name": "使用新账号登录验证凭据和角色",
        "method": "POST",
        "path": "/auth/login",
        "body": {"username": "{{system_user_phone}}", "password": "{{system_user_password}}"},
        "extract": ("school_user_token", "$.data.token"),
        "no_auth": True,
        "tests": [
            ("新账号登录：返回临时 Token（报告脱敏）", "pm.expect(__json.data.token).to.be.a('string').and.not.empty"),
            ("新账号登录：中文姓名和本轮角色正确", "pm.expect(__json.data.user.name).to.equal('刘明慧'); pm.expect(__json.data.user.roles).to.include(pm.variables.get('role_name'))"),
        ],
    },
    {
        "name": "按手机号回查新账号",
        "method": "GET",
        "path": "/school/system/users?schoolId=1&q={{system_user_phone}}",
        "tests": [
            ("账号列表：姓名、手机号、角色和备注正确", "const id=Number(pm.variables.get('system_user_id')); const row=__json.data.find(function(x){return Number(x.id)===id;}); pm.expect(row).to.exist; pm.expect(row.displayName).to.equal('刘明慧'); pm.expect(row.phone).to.equal(pm.variables.get('system_user_phone')); pm.expect(row.roles).to.include(pm.variables.get('role_name')); pm.expect(row.remark).to.equal('康源实验学校食品安全专员，负责日常台账复核。'); pm.expect(row.enabled).to.equal(true)"),
        ],
    },
    {
        "name": "编辑食品安全专员职责并保持角色",
        "method": "PATCH",
        "path": "/school/system/users/profile-with-roles",
        "body": {
            "id": "{{system_user_id}}",
            "patch": {"name": "刘明慧", "phone": "{{system_user_phone}}", "remark": "康源实验学校食品安全主管，负责台账复核与异常闭环。"},
            "roles": ["{{role_name}}"],
        },
        "tests": [("学校账号编辑：ok=true", "pm.expect(__json.data.ok).to.equal(true)")],
    },
    {
        "name": "停用食品安全专员账号",
        "method": "POST",
        "path": "/school/system/users/enabled",
        "body": {"id": "{{system_user_id}}", "enabled": False},
        "tests": [("账号停用：ok=true", "pm.expect(__json.data.ok).to.equal(true)")],
    },
    {
        "name": "回查账号编辑与停用结果",
        "method": "GET",
        "path": "/school/system/users?schoolId=1&q={{system_user_phone}}",
        "tests": [
            ("账号停用回查：职责、角色和状态正确", "const id=Number(pm.variables.get('system_user_id')); const row=__json.data.find(function(x){return Number(x.id)===id;}); pm.expect(row).to.exist; pm.expect(row.remark).to.equal('康源实验学校食品安全主管，负责台账复核与异常闭环。'); pm.expect(row.roles).to.deep.equal([pm.variables.get('role_name')]); pm.expect(row.enabled).to.equal(false)"),
        ],
    },
    {
        "name": "重新启用食品安全专员账号",
        "method": "POST",
        "path": "/school/system/users/enabled",
        "body": {"id": "{{system_user_id}}", "enabled": True},
        "tests": [("账号重新启用：ok=true", "pm.expect(__json.data.ok).to.equal(true)")],
    },
    {
        "name": "回查账号最终启用状态",
        "method": "GET",
        "path": "/school/system/users?schoolId=1&q={{system_user_phone}}",
        "tests": [
            ("账号最终状态：启用且中文职责保留", "const id=Number(pm.variables.get('system_user_id')); const row=__json.data.find(function(x){return Number(x.id)===id;}); pm.expect(row).to.exist; pm.expect(row.enabled).to.equal(true); pm.expect(row.remark).to.equal('康源实验学校食品安全主管，负责台账复核与异常闭环。')"),
        ],
    },
    {
        "name": "读取学校机构类型",
        "method": "GET",
        "path": "/school/system/org-type?schoolId=1",
        "extract": ("org_type", "$.data.orgType"),
        "tests": [("机构类型：仅允许 school 或 enterprise", "pm.expect(['school','enterprise']).to.include(__json.data.orgType)")],
    },
    {
        "name": "原值保存学校机构类型",
        "method": "POST",
        "path": "/school/system/org-type",
        "body": {"schoolId": 1, "orgType": "{{org_type}}"},
        "tests": [("机构类型持久化：ok=true", "pm.expect(__json.data.ok).to.equal(true)")],
    },
    {
        "name": "回查机构类型持久化结果",
        "method": "GET",
        "path": "/school/system/org-type?schoolId=1",
        "tests": [("机构类型回查：与保存前一致", "pm.expect(__json.data.orgType).to.equal(pm.variables.get('org_type'))")],
    },
    {
        "name": "查询账号启停审计日志",
        "method": "GET",
        "path": "/audit/logs?page=1&pageSize=20&eventType=account.user.enabled&outcome=success",
        "extract": ("system_audit_id", "$.data.items[0].id"),
        "tests": [
            ("审计列表：至少记录本轮账号启停", "pm.expect(__json.data.items).to.be.an('array').and.not.empty; pm.expect(Number(__json.data.total)).to.be.at.least(1); pm.expect(__json.data.items[0].event_type).to.equal('account.user.enabled'); pm.expect(__json.data.items[0].outcome).to.equal('success')"),
        ],
    },
    {
        "name": "读取账号启停审计详情",
        "method": "GET",
        "path": "/audit/logs/{{system_audit_id}}",
        "tests": [
            ("审计详情：事件、动作、租户和变更数据完整", "pm.expect(__json.data.event_type).to.equal('account.user.enabled'); pm.expect(__json.data.action).to.equal('update'); pm.expect(String(__json.data.tenant_id)).to.equal('1'); pm.expect(__json.data.after_data).to.be.an('object'); pm.expect(Number(__json.data.after_data.id)).to.equal(Number(pm.variables.get('system_user_id')))"),
        ],
    },
    {
        "name": "导出账号启停审计 CSV",
        "method": "GET",
        "path": "/audit/logs/export.csv?eventType=account.user.enabled&outcome=success",
        "tests": [
            ("审计 CSV：包含事件类型和成功结果", "pm.expect(__json.data.csv).to.include('account.user.enabled'); pm.expect(__json.data.csv).to.include('success')"),
        ],
    },
]


def main() -> None:
    api = shared.Apifox()
    by_name = {item["name"]: item for item in api.scenarios()}
    source = by_name["S00-环境与认证（双库）"]
    source_data = api.scenario(int(source["id"]))
    loop_template = source_data["steps"][0]
    login_template = loop_template["children"][0]
    request_template = loop_template["children"][1]

    current = by_name.get(NAME)
    scenario_id = int(current["id"]) if current else api.duplicate(int(source["id"]))
    scenario = api.scenario(scenario_id)
    scenario.update({"name": NAME, "description": DESCRIPTION, "folderId": shared.FOLDER_ID, "ordering": 130})
    loop = copy.deepcopy(loop_template)
    loop["id"] = shared.new_id()
    loop["number"] = 1
    loop.setdefault("parameters", {})["array"] = '["http://127.0.0.1:3553","http://127.0.0.1:3552"]'
    login = shared.make_login(login_template, PREFIX)
    login_pre = login["customHttpRequest"]["preProcessors"][0]["data"]
    login["customHttpRequest"]["preProcessors"][0]["data"] = (
        login_pre
        + "const __stamp = '20260926' + String(Date.now()).slice(-6);\n"
        + "const __tail = String(Date.now()).slice(-8);\n"
        + "pm.variables.set('business_batch', __stamp);\n"
        + "pm.variables.set('role_name', '学校食品安全数据管理员（档案' + __stamp + '）');\n"
        + "pm.variables.set('system_user_phone', '137' + __tail);\n"
        + "pm.variables.set('system_user_password', 'Shian-' + __tail + '-Aa1!');\n"
    )
    loop["children"] = [login]
    for number, operation in enumerate(OPERATIONS, start=2):
        step = shared.make_request(request_template, **operation)
        step["number"] = number
        loop["children"].append(step)
    scenario["steps"] = [loop]
    api.save(scenario)

    saved = api.scenario(scenario_id)
    requests = shared.flatten_requests(saved["steps"])
    business = requests[1:]
    if len(business) != len(OPERATIONS):
        raise RuntimeError(f"{NAME}: expected {len(OPERATIONS)} business requests, got {len(business)}")
    if not all(shared.request_has_auditable_evidence(request) for request in requests):
        raise RuntimeError(f"{NAME}: one or more requests lack auditable evidence")

    suite = api.test_suite()
    existing = [item for item in suite.get("items", []) if item.get("name") != NAME]
    existing.append({
        "id": shared.new_id(), "name": NAME, "type": "STATIC_TEST_SCENARIO",
        "testScenarios": [{"id": scenario_id, "options": {}}], "options": {},
    })
    suite["items"] = existing
    suite["description"] = (
        "学校端 S00-S13 已解除门禁场景的双库兼容一键串行入口。MySQL 仅允许 foodsafe_test，"
        "达梦仅允许 WWWWEB。S12 公告、附件和食堂配置已完成 DBCOMPAT-0021 双库原表与重启读回验证。"
    )
    suite.setdefault("options", {})["runMode"] = "serial"
    api.save_test_suite(suite)
    saved_suite = api.test_suite()
    linked = [item["testScenarios"][0]["id"] for item in saved_suite["items"]]
    if scenario_id not in linked:
        raise RuntimeError("S12 was not linked into the one-click test suite")
    print(f"{NAME}: id={scenario_id}, operations={len(business)}, totalRequests={len(requests)}")
    print(f"一键测试-安全入口: scenarios={len(linked)}, suiteId={shared.TEST_SUITE_ID}")


if __name__ == "__main__":
    main()
