#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""《客户拉通大表》DEMO · 产品介绍（自包含 HTML）验收脚本
================================================================
验收项（每项贴真实输出）：
  1. 渲染：外部请求 = 0、无 console error、SVG 节点 > 0；七段（01–07）均渲染
  2. 一致性抽查：5 个功能/字段名逐字存在（support_todos / replacement_cases /
     机会性判别三条 / 周会决议（三岗各填一栏）/ 三轨落盘）
  3. 关键词负面断言：页面不含「已上线」「生产环境正式运行」
  4. 截图 ≥2（上/下半页）供目视
  5. 保护文件 mtime 未变（构建不经手保护文件）

解释器：env -u PYTHONPATH /usr/bin/python3 脚本/verify_customer_thread_intro.py
"""
import io
import os
import sys
import time
from pathlib import Path

HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
if HERE not in sys.path:
    sys.path.insert(0, HERE)

TARGET = os.path.join(ROOT, '知识沉淀', '客户拉通大表_产品介绍_20261011.html')
SHOT_DIR = os.path.join(ROOT, '知识沉淀', '_intro_shots')
PROTECTED = [
    '工具/index.html',
    '工具/app.html',
    '工具/客户拉通大表_demo.html',
    '工具/客户拉通大表_仪表盘.html',
    '工具/天隆_2025vs2026_趋势看板.html',
]

# 一致性抽查：结构清单里逐字功能 / 字段名
CONSISTENCY = [
    ('集合 support_todos', 'support_todos'),
    ('集合 replacement_cases', 'replacement_cases'),
    ('机会性判别三条', '机会性判别三条'),
    ('周会决议三岗', '周会决议（三岗各填一栏）'),
    ('三轨落盘', '三轨落盘'),
]

NEGATIVE = ['已上线', '生产环境正式运行']
SECTIONS = ['sec-1', 'sec-2', 'sec-3', 'sec-4', 'sec-5', 'sec-6', 'sec-7']

PASS, FAIL = [], []


def ok(msg):
    PASS.append(msg)
    print('  ✅ ' + msg)


def bad(msg):
    FAIL.append(msg)
    print('  ❌ ' + msg)


def snapshot():
    out = {}
    for rel in PROTECTED:
        p = os.path.join(ROOT, rel)
        out[rel] = os.path.getmtime(p) if os.path.exists(p) else None
    return out


def run_build():
    """跑生成器，返回是否成功。"""
    import build_customer_thread_intro as B
    return B.main() == 0


def main():
    print('=' * 78)
    print('《客户拉通大表》DEMO · 产品介绍 · 验收')
    print('=' * 78)

    if not os.path.exists(TARGET):
        bad('产物不存在：%s' % TARGET)
        print('\n结论：FAIL')
        return 1

    # ---- 5. 保护文件 mtime：构建前后比对 ----
    print('\n[5] 保护文件 mtime 门禁')
    before = snapshot()
    run_build()  # 重跑生成器
    after = snapshot()
    for rel in PROTECTED:
        if before[rel] == after[rel]:
            ok('mtime 未变：%s' % rel)
        else:
            bad('mtime 被改动：%s（%s → %s）' % (rel, before[rel], after[rel]))

    html = io.open(TARGET, encoding='utf-8').read()

    # ---- 3. 关键词负面断言（静态 + 渲染后各一次）----
    print('\n[3] 关键词负面断言（静态）')
    for kw in NEGATIVE:
        if kw in html:
            bad('页面含禁用词：「%s」' % kw)
        else:
            ok('不含「%s」' % kw)

    # ---- 2. 一致性抽查（静态逐字）----
    print('\n[2] 一致性抽查（逐字存在）')
    for label, lit in CONSISTENCY:
        if lit in html:
            ok('%s → 逐字存在「%s」' % (label, lit))
        else:
            bad('%s → 缺失「%s」' % (label, lit))

    # ---- 1. 渲染（playwright）----
    print('\n[1] 渲染实测（playwright · chrome）')
    try:
        from playwright.sync_api import sync_playwright
    except Exception as e:
        bad('playwright 不可用：%s' % e)
        print('\n结论：FAIL')
        return 1

    externals, failed, console_errs, page_errs = [], [], [], []
    with sync_playwright() as p:
        br = p.chromium.launch(channel='chrome')
        pg = br.new_page(viewport={'width': 1440, 'height': 1100})
        pg.on('request', lambda r: externals.append(r.url) if r.url.startswith(('http://', 'https://')) else None)
        pg.on('requestfailed', lambda r: failed.append(r.url))
        pg.on('console', lambda m: console_errs.append(m.text) if m.type == 'error' else None)
        pg.on('pageerror', lambda e: page_errs.append(str(e)))
        pg.goto(Path(TARGET).as_uri())
        pg.wait_for_load_state('load')
        pg.wait_for_timeout(500)

        svg_n = pg.evaluate("document.querySelectorAll('svg').length")
        svg_el = pg.evaluate("document.querySelectorAll('svg *').length")
        rendered = pg.evaluate("""(() => {
            const ids = %s;
            const out = {};
            for (const id of ids) {
                const el = document.getElementById(id);
                out[id] = el ? (el.offsetHeight > 0 && el.textContent.trim().length > 20) : false;
            }
            return out;
        })()""" % str(SECTIONS).replace("'", '"'))
        body_text = pg.evaluate("document.body.innerText")

        # 截图（上 / 下）
        os.makedirs(SHOT_DIR, exist_ok=True)
        ts = time.strftime('%H%M%S')
        top = os.path.join(SHOT_DIR, 'intro_top_%s.png' % ts)
        bot = os.path.join(SHOT_DIR, 'intro_bottom_%s.png' % ts)
        pg.evaluate("window.scrollTo(0,0)")
        pg.wait_for_timeout(150)
        pg.screenshot(path=top)
        pg.evaluate("window.scrollTo(0, document.body.scrollHeight)")
        pg.wait_for_timeout(150)
        pg.screenshot(path=bot)
        height = pg.evaluate("document.body.scrollHeight")
        br.close()

    if len(externals) == 0:
        ok('外部请求 = 0')
    else:
        bad('外部请求 = %d：%s' % (len(externals), externals[:5]))
    if len(console_errs) == 0 and len(page_errs) == 0:
        ok('console error = 0（含 pageerror）')
    else:
        bad('console/pageerror：%s' % (console_errs + page_errs)[:5])
    if svg_n > 0 and svg_el > 0:
        ok('SVG 节点 > 0（svg=%d, 子节点=%d）' % (svg_n, svg_el))
    else:
        bad('SVG 节点 = 0（svg=%d, 子节点=%d）' % (svg_n, svg_el))

    miss = [s for s in SECTIONS if not rendered.get(s)]
    if not miss:
        ok('七段（%s）均渲染' % ' / '.join(s.replace('sec-', '0') for s in SECTIONS))
    else:
        bad('未渲染段落：%s' % miss)

    # 渲染后再做一次负面断言（防止 JS 注入）
    for kw in NEGATIVE:
        if kw in body_text:
            bad('渲染后正文含禁用词：「%s」' % kw)
        else:
            ok('渲染后正文不含「%s」' % kw)

    print('    页面高度 %d px ｜ 截图：\n      %s\n      %s' % (height, top, bot))

    print('\n' + '=' * 78)
    print('通过 %d 项 ｜ 失败 %d 项' % (len(PASS), len(FAIL)))
    if FAIL:
        print('失败清单：')
        for f in FAIL:
            print('  - ' + f)
        print('结论：FAIL')
        return 1
    print('结论：PASS ✅')
    return 0


if __name__ == '__main__':
    sys.exit(main())
