#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""验收「宝锐分子诊断原料知识库」页面：DOM 断言 + 交互实测 + 零外链 + 不截断。"""
import os
import re
import sys

from playwright.sync_api import sync_playwright

PAGE = '/Users/liuxinyuan/Desktop/Hermes输出-工作类/产品与市场/宝锐分子诊断原料知识库/index.html'
fails = []


def ck(name, ok, extra=''):
    print('  %s %s %s' % ('✅' if ok else '❌', name, extra))
    if not ok:
        fails.append(name)


with sync_playwright() as p:
    b = p.chromium.launch(channel='chrome')
    pg = b.new_page(viewport={'width': 1440, 'height': 950}, device_scale_factor=2)
    errs, reqs = [], []
    pg.on('pageerror', lambda e: errs.append(str(e)))
    pg.on('console', lambda m: errs.append('console:' + m.text) if m.type == 'error' else None)
    pg.on('request', lambda r: reqs.append(r.url))
    pg.goto('file://' + PAGE, wait_until='load')
    pg.wait_for_timeout(700)

    print('【1. 结构】')
    total = pg.evaluate("ITEMS.length")
    ck('数据载入 84 个核心货号', total == 84, '实际 %d' % total)
    rows = pg.eval_on_selector_all('#tb tr[data-i]', 'els=>els.length')
    ck('首屏表格渲染行数 == 货号数', rows == total, '行 %d' % rows)
    kpi = pg.eval_on_selector_all('#view-kb .kpi-n', 'els=>els.map(e=>e.textContent)')
    ck('货号库 KPI 卡 7 个', len(kpi) == 7, str(kpi))

    print('【2. 品牌】')
    ck('Logo 真解码', pg.evaluate("(()=>{const i=document.querySelector('.bior-logo-chip img');return !!(i&&i.complete&&i.naturalWidth>0)})()"))
    ck('slogan 页眉+页脚各一', pg.evaluate("(document.body.innerHTML.match(/守护生命健康/g)||[]).length") == 2)

    print('【3. 筛选与搜索】')
    pg.click('.chip[data-cat="LAMP"]')
    pg.wait_for_timeout(200)
    n = pg.eval_on_selector_all('#tb tr[data-i]', 'els=>els.length')
    ck('按 LAMP 筛选只剩 LAMP 货号', 0 < n <= 6, '%d 行' % n)
    pg.click('.chip[data-cat="ALL"]')
    pg.fill('#q', 'M3291')
    pg.wait_for_timeout(250)
    n1 = pg.eval_on_selector_all('#tb tr[data-i]', 'els=>els.length')
    ck('搜索 M3291 → 命中', 1 <= n1 <= 3, '%d 行' % n1)
    pg.fill('#q', '结核')
    pg.wait_for_timeout(250)
    n2 = pg.eval_on_selector_all('#tb tr[data-i]', 'els=>els.length')
    ck('搜索场景「结核」→ 命中', n2 >= 2, '%d 行' % n2)
    pg.fill('#q', '诺唯赞')
    pg.wait_for_timeout(250)
    n3 = pg.eval_on_selector_all('#tb tr[data-i]', 'els=>els.length')
    ck('搜索竞品「诺唯赞」→ 命中', n3 >= 10, '%d 行' % n3)
    pg.click('#rst')
    pg.wait_for_timeout(200)
    ck('重置恢复全量', pg.eval_on_selector_all('#tb tr[data-i]', 'els=>els.length') == total)

    print('【4. 详情抽屉（体系表/程序/案例/竞品）】')
    # 打开一个有说明书的货号（M2071: 体系表 + 反应程序）
    pg.evaluate("(()=>{const i=ITEMS.findIndex(r=>r['货号']==='M2071');open(i<0?0:i)})()")
    pg.wait_for_timeout(400)
    ck('抽屉打开', pg.evaluate("document.getElementById('dr').classList.contains('open')"))
    title = pg.evaluate("document.getElementById('dt').textContent")
    ck('标题为品名', len(title) > 5, title[:40])
    ntbl = pg.eval_on_selector_all('#db table.pt', 'els=>els.length')
    ck('渲染出体系/程序真表格', ntbl >= 2, '%d 张表' % ntbl)
    cells = pg.evaluate("(()=>{const t=document.querySelector('#db table.pt');return t?t.querySelectorAll('th,td').length:0})()")
    ck('表格有单元格内容', cells >= 6, '%d 格' % cells)
    pdf = pg.evaluate("(()=>{const a=document.querySelector('#db a[href^=\"说明书/\"]');return a?a.getAttribute('href'):''})()")
    ck('说明书本地链接带页码', pdf.startswith('说明书/') and '#page=' in pdf, pdf[:60])
    pg.screenshot(path='/tmp/v_kb_detail.png')
    # 关闭
    pg.click('#dclose')
    pg.wait_for_timeout(250)
    ck('抽屉可关闭', not pg.evaluate("document.getElementById('dr').classList.contains('open')"))

    print('【5. 无说明书货号（待补口径）】')
    import json as _json
    _d = _json.load(open('/Users/liuxinyuan/Desktop/Hermes输出-工作类/数据/diag_knowledge.json', encoding='utf-8'))
    _idx = next((i for i, r in enumerate(_d['items']) if not r['说明书文件名'] and r.get('同场景关联货号')), None)
    ck('存在「无说明书但有同场景关联」的货号', _idx is not None, 'index=%s' % _idx)
    pg.evaluate("(i)=>open(i)", _idx)
    pg.wait_for_timeout(350)
    body = pg.evaluate("document.getElementById('db').textContent")
    ck('标注「官网未发布说明书」而非编造', '官网未发布说明书' in body)
    ck('给出同场景关联货号', '可参考同场景体系货号' in body)

    print('【6. 无截断 / 无横向溢出】')
    ov = pg.evaluate("(()=>{let bad=[];document.querySelectorAll('table.grid td,table.grid th,#db .kv div,#db .quote').forEach(e=>{if(e.scrollWidth>e.clientWidth+2)bad.push(e.textContent.slice(0,18))});return bad})()")
    ck('文字列无截断', len(ov) == 0, str(ov[:3]))
    hscroll = pg.evaluate("document.documentElement.scrollWidth <= window.innerWidth+2")
    ck('1440 宽无横向溢出', hscroll)
    pg.set_viewport_size({'width': 400, 'height': 860})
    pg.wait_for_timeout(300)
    ck('400 宽无横向溢出', pg.evaluate("document.documentElement.scrollWidth <= window.innerWidth+3"))

    print('【7. 外链与资源】')
    ext_scripts = pg.evaluate("Array.from(document.querySelectorAll('script[src],link[href]')).map(e=>e.src||e.href).filter(u=>/^https?:/.test(u))")
    ck('零外部脚本/样式（自包含）', len(ext_scripts) == 0, str(ext_scripts[:2]))
    # 打开一个有官网说明书的货号，确认官网 PDF 链接存在
    pg.evaluate("(i)=>open(i)", next(i for i, r in enumerate(_d['items']) if r['说明书URL']))
    pg.wait_for_timeout(300)
    http_links = pg.evaluate("Array.from(document.querySelectorAll('#db a[href^=\"http\"]')).length")
    ck('详情内含官网说明书 PDF 链接', http_links >= 1, '%d 条' % http_links)
    pg.click('#dclose')
    from urllib.parse import unquote
    bad = []
    for u in reqs:
        if not u.startswith('file://'):
            continue
        path = unquote(u.replace('file://', '').split('?')[0].split('#')[0])
        if not os.path.exists(path):
            bad.append(u[:90])
    ck('无 404 本地资源', len(bad) == 0, str(bad[:2]))
    # 硬断言：详情页会引用的每一个 PDF 都必须在包内真实存在
    import json as _j
    _dd = _j.load(open('/Users/liuxinyuan/Desktop/Hermes输出-工作类/数据/diag_knowledge.json', encoding='utf-8'))
    _pkg = '/Users/liuxinyuan/Desktop/Hermes输出-工作类/产品与市场/宝锐分子诊断原料知识库/说明书'
    _want = {r.get('说明书本地文件') or r['说明书文件名'] for r in _dd['items']
             if r.get('说明书本地文件') or r['说明书文件名']}
    _miss = sorted(f for f in _want if not os.path.exists(os.path.join(_pkg, f)))
    ck('说明书 PDF 全部随包提供（引用=存在）', len(_miss) == 0, '缺 %d：%s' % (len(_miss), _miss[:3]))
    _noinfo = [r['货号'] for r in _dd['items'] if (r['说明书文件名'] or r['说明书URL']) and not (r.get('说明书本地文件') or r['说明书URL'])]
    ck('有说明书的货号至少有本地或官网来源', len(_noinfo) == 0, str(_noinfo[:3]))
    print('  ℹ 页面内 http 链接（说明书官网 PDF / 官网产品页）:',
          len(pg.evaluate("Array.from(document.querySelectorAll('a[href^=\"http\"]')).map(a=>a.href)")))

    print('【7.5 竞品对标 Tab】')
    pg.click('#tab-comp')
    pg.wait_for_timeout(400)
    ck('竞品视图显示', pg.evaluate("!document.getElementById('view-comp').hidden"))
    ck('货号库视图隐藏', pg.evaluate("document.getElementById('view-kb').hidden"))
    ncomp = pg.eval_on_selector_all('#comp-tb tr', 'els=>els.length')
    ck('竞品条目渲染（含表头行）', ncomp >= 40, '%d 行' % ncomp)
    brands = pg.evaluate("Array.from(document.querySelectorAll('#comp-brands .chip')).map(b=>b.textContent.trim())")
    ck('品牌筛选项含四家', sum(1 for b in brands if any(x in b for x in ('诺唯赞', '翌圣', '菲鹏', '全式金'))) >= 3, str(brands))
    ck('来源链接可点', pg.evaluate("document.querySelectorAll('#comp-tb a[href^=\"http\"]').length") >= 5)
    pg.fill('#cq', '可冻干'); pg.wait_for_timeout(300)
    ck('竞品搜索「可冻干」有效', pg.eval_on_selector_all('#comp-tb tr', 'els=>els.length') >= 5)
    pg.click('#comp-rst'); pg.wait_for_timeout(200)
    ck('重置恢复', pg.evaluate("document.getElementById('comp-cnt').textContent").find('匹配') >= 0)
    pg.screenshot(path='/tmp/v_kb_comp.png')
    pg.click('#tab-kb'); pg.wait_for_timeout(250)
    ck('可切回货号库', pg.evaluate("!document.getElementById('view-kb').hidden"))

    print('【7.8 产品资料说明书（QA 正式版）可用性】')
    _ai = next((i for i, r in enumerate(_d['items']) if r.get('说明书_产品资料文件') and not r.get('说明书文件名')), None)
    if _ai is None:
        ck('存在「仅产品资料说明书」的货号', False, 'None')
    else:
        ck('存在「仅产品资料说明书」的货号', True, 'index=%s' % _ai)
        pg.evaluate("(i)=>open(i)", _ai)
        pg.wait_for_timeout(350)
        _html = pg.evaluate("document.getElementById('db').innerHTML")
        ck('产品资料说明书渲染出本地 PDF 链接', ('说明书/' in _html) and ('#page=' in _html))
        ck('不再误报「官网未发布」', '官网未发布说明书 PDF' not in _html)
        pg.click('#dclose')

    print('【8. 运行时报错】')
    ck('page error == 0', len(errs) == 0, str(errs[:2]))

    b.close()

print()
print('=' * 60)
if fails:
    print('❌ 未通过 %d 项：%s' % (len(fails), '、'.join(fails)))
    sys.exit(1)
print('✅ 全部通过（%d 项断言）' % 24)
