#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""真实数据验收：9 月 2093 条（飞书基础数据）跑通控件收敛 + 四栏 KPI + 三级下钻 + 详情。"""
import io
import json
import sys
from playwright.sync_api import sync_playwright

ROOT = '/Users/liuxinyuan/Desktop/Hermes输出-工作类'
BASE = 'http://127.0.0.1:8765/'
sess = json.load(open(ROOT + '/数据/auth_sessions.json', encoding='utf-8'))
mine = sorted([(v['expire_at'], k, v) for k, v in sess.items() if v.get('name') == '刘新元'],
              reverse=True, key=lambda x: x[0])
TOKEN, IDENT = mine[0][1], dict(mine[0][2])
IDENT['open_id'] = 'ou_preview_only'
IDENT.pop('expire_at', None)

ok = True
def chk(label, cond, detail=''):
    global ok
    print('  %s %-46s %s' % ('✅' if cond else '❌', label, detail))
    if not cond:
        ok = False

with sync_playwright() as p:
    br = p.chromium.launch(channel='chrome', headless=True)
    pg = br.new_page(viewport={'width': 1500, 'height': 1050})
    errs = []
    pg.on('pageerror', lambda e: errs.append(str(e)))
    pg.on('dialog', lambda d: d.accept())
    pg.goto(BASE, wait_until='domcontentloaded')
    pg.evaluate("""([tk, idn]) => { localStorage.setItem('bior_auth_token', tk);
        localStorage.setItem('bior_sw_user_v2', JSON.stringify(idn)); }""", [TOKEN, IDENT])
    pg.goto(BASE + '工具/index.html', wait_until='load')
    pg.wait_for_timeout(2500)
    pg.evaluate("""() => { window.state = window.state || {};
        window.state.user = JSON.parse(JSON.stringify(%s));
        if (typeof showApp === 'function') showApp(); }""" % json.dumps(IDENT, ensure_ascii=False))
    pg.wait_for_timeout(1200)
    pg.evaluate("() => { visitView='dept'; visitDrill={dept:'',sales:''}; visitHubState='see'; state.tab='visitStats'; render(); }")
    pg.wait_for_timeout(1200)
    pg.evaluate("() => visitSetMonth('2026-09')")
    pg.wait_for_timeout(4500)

    def snap():
        return pg.evaluate("""() => { const b=document.getElementById('visitBody');
          const T=e=>e?e.innerText.replace(/\\s+/g,' ').trim():'';
          const ctrl=(document.querySelector('main')||document.body).innerText;
          return {txt:T(b), rows:[...b.querySelectorAll('tbody tr')].map(tr=>T(tr)),
                  crumb:T(document.getElementById('visitCrumb')), all:ctrl.replace(/\\s+/g,' ')}; }""")

    print('① 控件收敛（去掉重叠的「全队」）')
    s = snap()
    chk('有「👤 我的」', '我的' in s['all'])
    chk('有「🏢 按部门」', '按部门' in s['all'])
    chk('有「📋 按客户」', '按客户' in s['all'])
    chk('已无「全队」按钮', '全队' not in s['all'], '（页面全文不含「全队」）')
    pg.screenshot(path='/tmp/p3_1_head.png')

    print('\n② 四栏 KPI（真实 9 月数据）')
    chk('当月活动总数 2093', '2093' in s['all'], '')
    chk('线下拜访 403', '403' in s['all'])
    chk('线上跟进 67', '67' in s['all'])
    chk('形式未标 1623', '1623' in s['all'])
    chk('标注可判定率 22.5%', '22.5' in s['all'])
    chk('说明「未标不计入线下/线上」', '不计入' in s['all'])

    print('\n③ 部门表 + 守恒')
    chk('出现销售拓展部', '销售拓展部' in s['txt'])
    chk('出现生命科学销售部', '生命科学销售部' in s['txt'])
    chk('出现大客户部', '大客户部' in s['txt'])
    deps = pg.evaluate("() => (window.__vj.departments||[]).map(d=>({d:d.dept,o:d.offline,n:d.online,t:d.total,s:d.sales_count}))")
    so, sn = sum(d['o'] for d in deps), sum(d['n'] for d in deps)
    j = pg.evaluate("() => ({o:window.__vj.offline_count,n:window.__vj.online_count,t:window.__vj.total,judge:window.__vj.channel_judge,src:window.__vj.src_breakdown})")
    chk('守恒：部门线下之和 == KPI', so == j['o'], '%d vs %d' % (so, j['o']))
    chk('守恒：部门线上之和 == KPI', sn == j['n'], '%d vs %d' % (sn, j['n']))
    chk('来源标注飞书历史 2093', j['src'].get('feishu') == 2093, str(j['src']))
    print('     部门明细：', ' ｜ '.join('%s %d(线%d/线%d)' % (d['d'], d['t'], d['o'], d['n']) for d in deps))
    pg.screenshot(path='/tmp/p3_2_depts.png')

    print('\n④ 下钻：部门 → 销售')
    dep_top = pg.evaluate("""() => { const d=(window.__vj.departments||[]).find(x=>x.dept==='销售拓展部');
        return d&&d.sales&&d.sales.length?{name:d.sales[0].name,total:d.sales[0].total,cust:d.sales[0].customer_count}:null; }""")
    assert dep_top, '接口没给出销售拓展部的销售'
    print('     接口给出的第一位销售：%s（活动 %d 条 / 客户 %d 家）' % (dep_top['name'], dep_top['total'], dep_top['cust']))
    pg.evaluate("() => visitDrillTo('销售拓展部','')")
    pg.wait_for_timeout(1000)
    s = snap()
    chk('面包屑 全部部门 › 销售拓展部', '销售拓展部' in s['crumb'], s['crumb'][:70])
    chk('列出该部门销售（动态取第一位）', dep_top['name'] in s['txt'], '找「%s」' % dep_top['name'])
    chk('销售行带数字（非空态）', len(s['rows']) >= 5, '%d 行' % len(s['rows']))
    pg.screenshot(path='/tmp/p3_3_sales.png')

    print('\n⑤ 下钻：销售 → 客户')
    pg.evaluate("(n) => visitDrillTo('销售拓展部', n)", dep_top['name'])
    pg.wait_for_timeout(1200)
    s = snap()
    chk('面包屑到销售级', dep_top['name'] in s['crumb'], s['crumb'][:80])
    chk('列出该销售的客户', len(s['rows']) >= 1, '%d 行' % len(s['rows']))
    print('     前 3 行：', ' / '.join(r[:46] for r in s['rows'][:3]))
    pg.screenshot(path='/tmp/p3_4_cust.png')

    print('\n⑥ 活动明细 + 看详情（飞书历史）')
    pg.evaluate("() => { const b=document.querySelector('#visitBody tbody tr button'); if(b) b.click(); }")
    pg.wait_for_timeout(800)
    n_acts = pg.evaluate("() => document.querySelectorAll('#visitBody tbody td div[id^=visitDetail]').length")
    chk('展开出活动明细', n_acts >= 1, '%d 条' % n_acts)
    acts_txt = pg.evaluate("() => { const t=document.querySelector('#visitBody tbody td div[id^=visitActs], #visitBody tbody td[colspan]'); return t?t.innerText.replace(/\\s+/g,' ').trim():''; }")
    chk('明细带「飞书历史」标签', '飞书历史' in acts_txt, acts_txt[:60])
    pg.evaluate("() => { const a=[...document.querySelectorAll('#visitBody a')].find(x=>x.innerText.indexOf('看详情')>=0); if(a) a.click(); }")
    pg.wait_for_timeout(2200)
    det = pg.evaluate("() => { const d=document.querySelector('#visitBody td div[id^=visitDetail]'); return d?d.innerText.replace(/\\s+/g,' ').trim():''; }")
    chk('详情渲染出正文', '【来源】飞书历史活动' in det, det[:64])
    chk('详情标注活动形式', '活动形式' in det)
    pg.screenshot(path='/tmp/p3_5_detail.png')

    print('\n⑦ 维度切换：我的 / 按客户')
    pg.evaluate("() => visitSetView('me')")
    pg.wait_for_timeout(3500)
    s = snap()
    mine_n = pg.evaluate("() => (window.__vj.customers||[]).length")
    chk('「我的」客户清单出现且少于全队', ('家客户' in s['all']) and 0 < mine_n < 369, '我的 %d 家' % mine_n)
    pg.evaluate("() => visitSetView('cust')")
    pg.wait_for_timeout(3500)
    s = snap()
    chk('切「按客户」出 369 家', '369' in s['all'] or '368' in s['all'])
    chk('筛选框可用', pg.evaluate("() => !!document.querySelector('#visitBody input[placeholder]')"))
    pg.evaluate("""() => { const i=document.querySelector('#visitBody input[placeholder]');
        i.value='丽珠'; visitSetQ('丽珠'); renderVisitBody(window.__vj); }""")
    pg.wait_for_timeout(600)
    s2 = snap()
    chk('筛选后只剩含「丽珠」的客户', '丽珠' in s2['txt'] and len(s2['rows']) < len(s['rows']),
        '%d → %d 行' % (len(s['rows']), len(s2['rows'])))
    pg.screenshot(path='/tmp/p3_6_cust_flat.png')

    print('\n⑧ 无 JS 错误')
    chk('0 JS 错误', len(errs) == 0, str(errs[:2]))
    br.close()

print('\n%s' % ('✅ 全部通过' if ok else '❌ 有失败项'))
sys.exit(0 if ok else 1)
