#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""研发填写板块 · 验收脚本（客户拉通大表 demo）。

验收项：
  ① 渲染：外部请求 0 / page error 0 / console error 0 / 横向不溢出
  ② 板块与弹层存在，且位于第二层
  ③ 视角切换：负责人=原值可见；技术支持/销售=原值隐藏且脱敏版无原始敏感串
  ④ 脱敏规则逐条实测（名称代号化 / 数值区间化 / 倍数档位化 / 批号截尾）
  ⑤ 规则表可增删 + 导出 JSON 可解析
  ⑥ 提交三轨：飞书描述不含原值、CloudBase 文档含 raw_scope 且原值只在密文字段
  ⑦ 审计流水随查看原值增长

用法：env -u PYTHONPATH /usr/bin/python3 脚本/verify_rd_block.py [目标.html]
"""
import json
import os
import re
import sys

ROOT = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
HTML = sys.argv[1] if len(sys.argv) > 1 else os.path.join(
    ROOT, '工具', '客户拉通大表_demo_v8_研发填写板块.html')
SHOTS = os.path.expanduser('~/.hermes/profiles/zone-b/cache/scratch/rd_shots')
os.makedirs(SHOTS, exist_ok=True)

RAW_HEAD = '体系 10× 浓缩；酶加量 1.2×；保护剂 海藻糖 8%(w/v) + 甘露醇 3%(w/v)；批号 20260914'
FORBIDDEN = ['海藻糖', '甘露醇', '20260914', '10×', '1.2×', '8%(w/v)']

ok_all = True


def line(tag, msg):
    global ok_all
    if tag == '❌':
        ok_all = False
    print('%s %s' % (tag, msg))


with open(HTML, encoding='utf-8') as f:
    PAGE = f.read()

print('═' * 74)
print('① 渲染 + 基础结构')
print('═' * 74)
from playwright.sync_api import sync_playwright

ext, errs, console = [], [], []
with sync_playwright() as p:
    b = p.chromium.launch(channel='chrome')
    pg = b.new_page(viewport={'width': 1440, 'height': 1000}, device_scale_factor=1)
    pg.on('request', lambda r: ext.append(r.url) if not r.url.startswith('file://') else None)
    pg.on('console', lambda m: console.append(m.type + ': ' + m.text) if m.type in ('error',) else None)
    pg.on('pageerror', lambda e: errs.append(str(e)))
    pg.goto('file://' + HTML, wait_until='networkidle')
    pg.wait_for_timeout(400)

    # 切到第二层
    pg.evaluate("document.querySelector('.tab[data-t=t2]') && document.querySelector('.tab[data-t=t2]').click()")
    pg.wait_for_timeout(300)
    pg.evaluate("document.querySelectorAll('.tab').forEach(function(t){ if(t.textContent.indexOf('第二层')>=0) t.click(); })")
    pg.wait_for_timeout(300)

    block_in_l2 = pg.evaluate(
        "!!(document.querySelector('#panel-l2 .rd-ident') && document.querySelector('#panel-l2 #rdSubmit')"
        " && !document.querySelector('#panel-l1 .rd-ident'))")
    box = pg.evaluate("({w:document.documentElement.scrollWidth,h:document.documentElement.scrollHeight})")
    if rows := pg.evaluate("document.querySelectorAll('#rdRuleTable tr').length"):
        pass

    line('✅' if not ext else '❌', '外部请求数 = %d %s' % (len(ext), ext[:3]))
    line('✅' if not errs else '❌', 'page error = %d %s' % (len(errs), errs[:2]))
    line('✅' if not console else '❌', 'console error = %d %s' % (len(console), console[:3]))
    line('✅' if block_in_l2 else '❌', '板块位于第二层且提交按钮在位 = %s' % block_in_l2)
    line('✅' if box['w'] <= 1440 else '❌', '横向 scrollWidth=%d（须 ≤1440）' % box['w'])

    print()
    print('═' * 74)
    print('③ 视角切换（原值可见性 = 项目负责人 / 管理员）')
    print('═' * 74)
    views = {}
    for v in ('owner', 'tech', 'sales', 'admin'):
        pg.evaluate("document.querySelector('.rd-idb[data-view=\"%s\"]').click()" % v)
        pg.wait_for_timeout(150)
        views[v] = pg.evaluate("""(() => {
            const raw = document.getElementById('rd_raw');
            const mask = document.getElementById('rdRawMask');
            const pv = window.__rdPv || {};
            const pvRawRow = document.getElementById('rdPvRawRow');
            return {rawShown: getComputedStyle(raw).display !== 'none' && !raw.disabled,
                    rawHiddenDisplay: getComputedStyle(raw).display === 'none',
                    maskShown: getComputedStyle(mask).display !== 'none',
                    maskText: mask.textContent, pvRawRowShown: getComputedStyle(pvRawRow).display !== 'none',
                    pvOut: document.getElementById('rdPvOut').textContent,
                    submitDisabled: document.getElementById('rdSubmit').disabled,
                    hits: pv.hits, lockTag: document.getElementById('rdLockTag').textContent};
        })()""")

    line('✅' if views['owner']['rawShown'] and views['owner']['pvRawRowShown'] else '❌',
         '负责人：原值可编辑=%s ｜ 预览含原值行=%s' % (views['owner']['rawShown'], views['owner']['pvRawRowShown']))
    line('✅' if views['admin']['rawShown'] and views['admin']['pvRawRowShown'] else '❌',
         '管理员：原值可编辑=%s ｜ 预览含原值行=%s' % (views['admin']['rawShown'], views['admin']['pvRawRowShown']))
    for v in ('tech', 'sales'):
        vv = views[v]
        bad = [t for t in FORBIDDEN if t in vv['maskText'] or t in vv['pvOut']]
        line('✅' if vv['rawHiddenDisplay'] and not vv['pvRawRowShown'] and vv['submitDisabled'] and not bad else '❌',
             '%s：原值隐藏=%s ｜ 预览隐藏原值行=%s ｜ 提交禁用=%s ｜ 脱敏版残留敏感串=%s ｜ 标记「%s」'
             % (v, vv['rawHiddenDisplay'], not vv['pvRawRowShown'], vv['submitDisabled'], bad, vv['lockTag']))
    line('✅' if views['owner']['hits'] > 0 else '❌', '脱敏命中数（负责人视角）= %s' % views['owner']['hits'])

    print()
    print('═' * 74)
    print('④ 脱敏规则逐条实测（目标串 → 实际输出）')
    print('═' * 74)
    masked = views['owner']['pvOut']
    exp = [('海藻糖 → 添加剂A', '添加剂A' in masked and '海藻糖' not in masked),
           ('甘露醇 → 添加剂B', '添加剂B' in masked and '甘露醇' not in masked),
           ('8%(w/v) → ±20% 区间', '6.4–9.6%' in masked),
           ('3%(w/v) → ±20% 区间', '2.4–3.6%' in masked),
           ('10× → 倍数档位', '高倍浓缩体系' in masked),
           ('1.2× → ±20% 区间', '0.96–1.44×' in masked),
           ('批号 20260914 → 截尾', '批号 ……0914' in masked and '20260914' not in masked)]
    # 带单位用量：5mM → ±20% 区间（防「只换名称、用量照旧」）
    pg.evaluate("""(() => {
        const raw = document.getElementById('rd_raw');
        raw.value = 'MgCl2 5mM；酶 0.5U/μL；保护剂 2mg/mL';
        raw.dispatchEvent(new Event('input', {bubbles:true}));
    })()""")
    pg.wait_for_timeout(200)
    masked2 = pg.evaluate("window.__rdPv.out")
    exp += [('5mM → ±20% 区间', '4–6mM' in masked2 or '4.0–6.0mM' in masked2),
            ('0.5U/μL → ±20% 区间', '0.4–0.6U/μL' in masked2),
            ('2mg/mL → ±20% 区间', '1.6–2.4mg/mL' in masked2)]
    pg.evaluate("""(() => {
        const raw = document.getElementById('rd_raw');
        raw.value = '体系 10× 浓缩；酶加量 1.2×；保护剂 海藻糖 8%(w/v) + 甘露醇 3%(w/v)；批号 20260914';
        raw.dispatchEvent(new Event('input', {bubbles:true}));
    })()""")
    pg.wait_for_timeout(200)
    masked = pg.evaluate("window.__rdPv.out")
    for name, good in exp[-3:]:
        line('✅' if good else '❌', '规则 %s' % name)
    exp = exp[:-3]
    for name, good in exp:
        line('✅' if good else '❌', '规则 %s' % name)
    print('   脱敏后文本：%s' % masked.replace('\n', ' ')[:150])

    print()
    print('═' * 74)
    print('⑤ 规则表可维护 + 导出')
    print('═' * 74)
    n_rows = pg.evaluate("document.querySelectorAll('#rdRuleTable tr').length")
    pg.evaluate("""(() => {
        document.getElementById('rdRuleAdd').click();
        const rows = document.querySelectorAll('#rdRuleTable tr');
        const last = rows[rows.length - 5];            /* 末条 dict 行（其后 4 行是内置） */
        const ins = last.querySelectorAll('input');
        ins[0].value = 'MgCl2'; ins[1].value = '添加剂Z';
        ins[0].dispatchEvent(new Event('input', {bubbles:true}));
        ins[1].dispatchEvent(new Event('input', {bubbles:true}));
    })()""")
    pg.wait_for_timeout(200)
    pg.evaluate("""(() => {
        const raw = document.getElementById('rd_raw');
        raw.value = raw.value + '；MgCl2 5mM';
        raw.dispatchEvent(new Event('input', {bubbles:true}));
    })()""")
    pg.wait_for_timeout(200)
    mg = pg.evaluate("window.__rdPv.out")
    line('✅' if n_rows >= 9 else '❌', '规则表行数 = %d（8 组名称对照 + 4 条内置 + 表头）' % n_rows)
    line('✅' if '添加剂Z' in mg and 'MgCl2' not in mg else '❌', '新增规则即时生效（MgCl2 → 添加剂Z）：%s' % ('添加剂Z' in mg))
    js = pg.evaluate("document.getElementById('rdRuleExport').click(); window.__rdRulesJson")
    try:
        payload = json.loads(js)
        good = payload.get('rules', {}).get('dict') and payload.get('rules', {}).get('builtin')
        line('✅' if good else '❌', '导出规则表可解析：dict %d 条 / builtin %d 条 / 版本 %s'
             % (len(payload['rules']['dict']), len(payload['rules']['builtin']), payload.get('rules_version')))
    except Exception as e:
        line('❌', '导出规则表 JSON 解析失败：%s' % e)

    print()
    print('═' * 74)
    print('⑥ 提交三轨（跨部门出口必须是脱敏版）')
    print('═' * 74)
    pg.evaluate("document.querySelector('.rd-idb[data-view=\"owner\"]').click()")
    pg.wait_for_timeout(150)
    pg.evaluate("document.getElementById('rdSubmit').click()")
    pg.wait_for_timeout(300)
    res = pg.evaluate("window.__rdLastSubmit")
    modal_open = pg.evaluate("document.getElementById('rdModal').classList.contains('open')")
    feishu = (res or {}).get('feishu', '')
    cb = (res or {}).get('cb', {}).get('doc', {})
    bad_f = [t for t in FORBIDDEN if t in feishu]
    line('✅' if modal_open else '❌', '弹层打开 = %s' % modal_open)
    line('✅' if res and not bad_f else '❌', '飞书描述不含原值（残留 %s）｜ 记录号 %s' % (bad_f, (res or {}).get('recId')))
    line('✅' if cb.get('raw_scope') == ['project_owner', 'admin'] else '❌',
         'raw_scope = %s（须 project_owner+admin）' % cb.get('raw_scope'))
    line('✅' if cb.get('raw_cipher', '').startswith('[原值') and '海藻糖' not in json.dumps(cb, ensure_ascii=False)
         else '❌', '共享文档里原值只在密文字段（raw_cipher 占位、正文无原值）')
    line('✅' if '添加剂A' in cb.get('raw_masked', '') else '❌', '共享文档 raw_masked 为脱敏版')
    line('✅' if len(feishu) > 100 and '脱敏版' in feishu else '❌', '飞书描述已生成（%d 字符）' % len(feishu))

    print()
    print('═' * 74)
    print('⑦ 审计流水')
    print('═' * 74)
    aud = pg.evaluate("window.__rdAudit || []")
    blocked = [a for a in aud if '拦截' in a.get('msg', '')]
    line('✅' if len(aud) >= 5 else '❌', '审计条数 = %d（含拦截尝试 %d 条）' % (len(aud), len(blocked)))
    print('   最近 3 条：')
    for a in aud[:3]:
        print('     %s ｜ %s ｜ %s' % (a['t'], a['who'], a['msg']))

    # ── 取证截图（先关掉提交弹层，避免遮挡）──
    pg.evaluate("document.getElementById('rdModal').classList.remove('open'); document.getElementById('mask').classList.remove('open');")
    pg.wait_for_timeout(200)
    pg.evaluate("document.querySelector('.rd-idb[data-view=\"owner\"]').click()")
    pg.wait_for_timeout(200)
    pg.locator('#panel-l2 .rd-ident').screenshot(path=os.path.join(SHOTS, '01_视角切换条.png'))
    pg.locator('#panel-l2 .rd-grid').screenshot(path=os.path.join(SHOTS, '02_研发四栏_负责人视角.png'))
    pg.evaluate("document.querySelector('.rd-idb[data-view=\"tech\"]').click()")
    pg.wait_for_timeout(250)
    pg.locator('#panel-l2 .rd-grid').screenshot(path=os.path.join(SHOTS, '03_研发四栏_技术支持视角.png'))
    pg.locator('#panel-l2 .rd-preview').screenshot(path=os.path.join(SHOTS, '04_脱敏预览.png'))
    pg.locator('#panel-l2 .rd-rules').screenshot(path=os.path.join(SHOTS, '05_规则表.png'))
    b.close()

print()
print('═' * 74)
print('截  图：%s' % SHOTS)
print('总结论：%s' % ('✅ 全部通过' if ok_all else '❌ 存在失败项'))
print('═' * 74)
sys.exit(0 if ok_all else 1)
