#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""2026Q3 数据台 验收：口令门 / 五个 tab / 搜索 / CSV / 零外链 / 零截断 / 截图。"""
import json
import os
import re
import sys

from playwright.sync_api import sync_playwright

HTML = '/Users/liuxinyuan/Desktop/Hermes输出-工作类/经营分析/2026Q3汇报数据台.html'
SHOTS = os.path.expanduser('~/.hermes/profiles/zone-b/cache/scratch/q3_tab_shots')
PW = 'biori2026'
os.makedirs(SHOTS, exist_ok=True)
ok = True


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


PAGE = open(HTML, encoding='utf-8').read()
ext, errs, console = [], [], []
with sync_playwright() as p:
    b = p.chromium.launch(channel='chrome')
    pg = b.new_page(viewport={'width': 1600, '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('pageerror', lambda e: errs.append(str(e)))
    pg.on('console', lambda m: console.append(m.type + ': ' + m.text) if m.type == 'error' else None)
    pg.goto('file://' + HTML, wait_until='networkidle')
    pg.wait_for_timeout(300)

    print('═' * 78)
    print('① 口令门')
    print('═' * 78)
    gate = pg.evaluate("!!document.getElementById('gate')")
    pg.fill('#pw', 'wrong-pass')
    pg.click('#go')
    pg.wait_for_timeout(400)
    err = pg.evaluate("document.getElementById('err') ? document.getElementById('err').textContent : '(门已开)'")
    line('✅' if gate else '❌', '初始显示口令门 = %s' % gate)
    line('✅' if '不正确' in err else '❌', '错误口令被拦：%s' % err)
    pg.fill('#pw', PW)
    pg.click('#go')
    pg.wait_for_timeout(500)
    gate2 = pg.evaluate("!!document.getElementById('gate')")
    tabs = pg.evaluate("document.querySelectorAll('.tab').length")
    line('✅' if not gate2 else '❌', '正确口令开门 = %s' % (not gate2))
    line('✅' if tabs == 10 else '❌', 'tab 数 = %d（应为 10：总览/ERP对账/商机/逾期应收/逾期分档/难点聚类/生命科学部/客户季度/月度/口径）' % tabs)

    print()
    print('═' * 78)
    print('② 五个数据 tab + 口径页')
    print('═' * 78)
    names = pg.evaluate("[...document.querySelectorAll('.tab')].map(t=>t.textContent)")
    for i, nm in enumerate(names):
        pg.evaluate("document.querySelectorAll('.tab')[%d].click()" % i)
        pg.wait_for_timeout(350)
        rows = pg.evaluate("document.querySelectorAll('#view table tbody tr').length")
        kpis = pg.evaluate("document.querySelectorAll('#view .kpi').length")
        cards = pg.evaluate("document.querySelectorAll('#view .card').length")
        txt = pg.evaluate("document.getElementById('view').innerText.length")
        line('✅' if txt > 200 else '❌', '%-8s 卡片 %d ｜ 表行 %d ｜ KPI %d ｜ 文本 %d 字' % (nm, cards, rows, kpis, txt))

    print()
    print('═' * 78)
    print('②b 表格列宽与长文本（本轮改造：长文本自动换行 + 列宽按类型分配）')
    print('═' * 78)
    pg.evaluate("[...document.querySelectorAll('.tab')].forEach(t=>{ if(t.textContent=='生命科学部') t.click(); })")
    pg.wait_for_timeout(500)
    cg = pg.evaluate("document.querySelectorAll('#view colgroup').length")
    line('✅' if cg >= 3 else '❌', 'colgroup 数 = %d（LS 三张表应各有列宽定义）' % cg)
    widths = pg.evaluate("(() => { const c=document.querySelectorAll('#view colgroup'); return c.length? [...c[0].children].map(x=>x.style.width):[]; })()")
    line('✅' if len(set(widths)) > 1 else '❌', '同表列宽非等宽 = %s（%s）' % (len(set(widths)) > 1, '/'.join(widths[:6])))
    nlong = pg.evaluate("document.querySelectorAll('#view td.long').length")
    line('✅' if nlong > 0 else '❌', '长文本单元格数 = %d（>0 表示类型推断与 long 样式生效）' % nlong)
    over = pg.evaluate("(() => { let bad=0; document.querySelectorAll('#view td.long').forEach(td => { if (td.scrollWidth > td.clientWidth + 2) bad++; }); return bad; })()")
    line('✅' if over == 0 else '❌', '长文本单元格横向溢出数 = %d（应为 0）' % over)
    j = json.load(open('/Users/liuxinyuan/Desktop/Hermes输出-工作类/数据/2026Q3汇报_汇总.json', encoding='utf-8'))
    kt = (j.get('tables') or {}).get('生命科学_课题组') or []
    probes = []
    if kt:
        for fld in ('导师研究方向', '痛点', '上半年开发计划', '下半年目标保障难点及措施'):
            for row in kt[:5]:
                v = re.sub(r'\s+', ' ', str(row.get(fld) or '')).strip()
                if len(v) >= 20:
                    probes.append((fld, v[:20]))
                    break
    seen = re.sub(r'\s+', ' ', pg.evaluate("document.getElementById('view').innerText"))
    for fld, p in probes:
        line('✅' if p in seen else '❌', '长文本原文完整可见（%s · 前 20 字）' % fld)
    line('✅' if len(probes) >= 2 else '❌', '长文本探针数 = %d（至少 2 个才有说服力）' % len(probes))
    line('✅' if '…' not in seen else '❌', 'LS tab 可见文本含「…」= %s' % ('…' in seen))
    print()
    print('②c 三档宽度：页面无横向溢出')
    for W in (1440, 1200, 390):
        pg.set_viewport_size({'width': W, 'height': 900})
        pg.wait_for_timeout(400)
        sw = pg.evaluate("document.documentElement.scrollWidth")
        line('✅' if sw <= W + 2 else '❌', 'viewport %d → documentElement.scrollWidth = %d' % (W, sw))
    pg.set_viewport_size({'width': 1600, 'height': 1000})
    pg.wait_for_timeout(300)

    print()
    print('═' * 78)
    print('③ 交互（搜索 / 排序 / 清除 / 导出）')
    print('═' * 78)
    pg.evaluate("[...document.querySelectorAll('.tab')].forEach(t=>{ if(t.textContent=='商机明细') t.click(); })")
    pg.wait_for_timeout(300)
    before = pg.evaluate("document.querySelectorAll('#view table tbody tr').length")
    pg.fill('#q', '陈溪子')
    pg.wait_for_timeout(450)
    after = pg.evaluate("document.querySelectorAll('#view table tbody tr').length")
    line('✅' if 0 < after <= before else '❌', '搜索「陈溪子」：%d → %d 行' % (before, after))
    pg.fill('#q', '')
    pg.wait_for_timeout(400)
    pg.evaluate("document.querySelector('#view table tbody tr th, th[data-k]') && document.querySelectorAll('th[data-k]')[3].click()")
    pg.wait_for_timeout(300)
    line('✅' if not pg.evaluate("document.querySelectorAll('#view table tbody tr').length") == 0 else '❌', '按表头排序后仍有数据')
    pg.click('#csv')
    pg.wait_for_timeout(400)
    line('✅' if not errs else '❌', 'CSV 导出未抛错（page error %d）' % len(errs))

    print()
    print('═' * 78)
    print('④ 渲染 / 安全基线')
    print('═' * 78)
    box = pg.evaluate("({w:document.documentElement.scrollWidth,h:document.documentElement.scrollHeight})")
    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[:2]))
    line('✅' if box['w'] <= 1620 else '❌', '页面 scrollWidth = %d（表内横向滚动，页面不溢出）' % box['w'])
    vis = re.sub(r'<style.*?</style>', '', PAGE, flags=re.S)
    vis = re.sub(r'<script.*?</script>', '', vis, flags=re.S)
    line('✅' if '…' not in vis else '❌', '页面可见静态文本残留「…」= %d' % vis.count('…'))
    line('✅' if '守护生命健康' in PAGE else '❌', 'Logo + slogan 在位（brand 套件）')

    print()
    print('═' * 78)
    print('⑤ 截图')
    print('═' * 78)
    pg.reload(wait_until='networkidle')
    pg.locator('#gate .box').screenshot(path=os.path.join(SHOTS, '01_口令门.png'))
    pg.fill('#pw', PW)
    pg.click('#go')
    pg.wait_for_timeout(500)
    pg.screenshot(path=os.path.join(SHOTS, '02_总览.png'))
    for nm, fn in (('ERP对账', '08_ERP对账.png'), ('商机明细', '03_商机明细.png'), ('逾期应收', '04_逾期应收.png'),
                   ('逾期分档', '05_逾期分档.png'), ('难点聚类', '06_难点聚类.png'),
                   ('口径与数据源', '07_口径与缺口.png')):
        pg.evaluate("[...document.querySelectorAll('.tab')].forEach(t=>{ if(t.textContent=='%s') t.click(); })" % nm)
        pg.wait_for_timeout(400)
        pg.screenshot(path=os.path.join(SHOTS, fn))
    b.close()
    for f in sorted(os.listdir(SHOTS)):
        print('   %s %.0f KB' % (f, os.path.getsize(os.path.join(SHOTS, f)) / 1024.0))

print()
print('总结论：%s' % ('✅ 全部通过' if ok else '❌ 存在失败项'))
sys.exit(0 if ok else 1)
