#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""验收「课题组经营」LS 专属 Tab：专属隔离 / 前端算 == 权威 JSON / 销售只见自己。"""
import io
import json
import sys
from datetime import datetime

from playwright.sync_api import sync_playwright

U = 'http://127.0.0.1:8765/工具/index.html'
GD = json.load(io.open('/Users/liuxinyuan/Desktop/Hermes输出-工作类/数据/ls_groups.json', encoding='utf-8'))
K = GD['kpi']
OK = True


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


def expect(name):
    """按可见范围复算，作为权威期望值。"""
    gs = [g for g in GD['groups'] if (g.get('orders') or 0) > 0]
    if name:
        gs = [g for g in gs if g.get('sales') == name]
    asof = datetime.strptime(K['as_of'], '%Y-%m-%d')
    paid = [float(g['amt']) for g in gs if (float(g.get('amt') or 0)) > 0]
    amt = sum(float(g.get('amt') or 0) for g in gs)
    y25 = sum(float(g.get('amt_p25') or 0) for g in gs)
    y26 = sum(float(g.get('amt_p26') or 0) for g in gs)
    rep = sum(1 for g in gs if (g.get('orders_paid') or 0) >= 2)
    slp = [g for g in gs if g.get('last_any') and (asof - datetime.strptime(g['last_any'], '%Y-%m-%d')).days > K['sleep']['days']]
    return {'conv': 100 * len(paid) / len(gs), 'paid': len(paid), 'all': len(gs), 'sleep': len(slp),
            'amt': amt, 'yoy': 100 * (y26 - y25) / y25 if y25 else None,
            'rep': rep, 'per': amt / len(paid) if paid else 0}


def boot(pg, name, role, line):
    pg.goto(U, wait_until='load')
    pg.wait_for_timeout(800)
    pg.evaluate("""async (p) => {
        document.getElementById('loginOverlay').style.display='none';
        document.getElementById('app').style.display='block';
        state.user = {name:p.name, role:p.role, dept:'销售管理', line:p.line};
        state.currentLine = p.line;
        var d = await fetch('../数据/ls_groups.json?v='+Date.now(), {cache:'no-store'}).then(r=>r.json());
        state.data = state.data || {}; state.data.lsGroups = d;
        try { if (typeof renderNav === 'function') renderNav(); } catch(e) {}
        state.tab = 'lsKpi'; render();
    }""", {'name': name, 'role': role, 'line': line})
    pg.wait_for_timeout(600)


def cards(pg):
    return pg.evaluate("() => { var h=document.querySelector('main'); if(!h) return {}; "
                       "var o={}; document.querySelectorAll('.stat-card').forEach(function(c){"
                       "o[c.querySelector('.label').innerText.trim()]=c.querySelector('.value').innerText.trim();}); return o; }")


with sync_playwright() as p:
    br = p.chromium.launch(channel='chrome', headless=True)
    pg = br.new_page(viewport={'width': 1500, 'height': 1100})
    errs = []
    pg.on('pageerror', lambda e: errs.append(str(e)))

    print('① LS 专属隔离')
    boot(pg, '刘新元', 'admin', 'da')
    chk('da 线导航无「课题组经营」', '课题组经营' not in pg.evaluate("() => document.querySelector('#sidebar').innerText"))
    boot(pg, '刘新元', 'admin', 'ls')
    chk('ls 线导航有「课题组经营」', '课题组经营' in pg.evaluate("() => document.querySelector('#sidebar').innerText"))

    print('\n② 管理员：前端算 == 权威 JSON')
    e = expect(None)
    c = cards(pg)
    print('    页面卡片：', c)
    chk('转化率 = %.2f%%' % e['conv'], c.get('🎯 测试→成交转化率', '').startswith('%.2f' % e['conv']), c.get('🎯 测试→成交转化率'))
    chk('沉睡 = %d' % e['sleep'], c.get('😴 沉睡课题组') == str(e['sleep']), c.get('😴 沉睡课题组'))
    chk('累计成交额 = %.2f万' % (e['amt'] / 1e4), c.get('💵 累计成交额', '').startswith('%.2f' % (e['amt'] / 1e4)), c.get('💵 累计成交额'))
    chk('同比 = %+.2f%%' % e['yoy'], ('%+.1f%%' % e['yoy']) in pg.evaluate("() => document.querySelector('main').innerText"), '%+.1f%%' % e['yoy'])
    chk('复购 = %d' % e['rep'], c.get('🔁 复购课题组') == str(e['rep']), c.get('🔁 复购课题组'))
    chk('单组产出 = ¥%s' % format(round(e['per']), ','), c.get('📦 单组产出') == '¥' + format(round(e['per']), ','), c.get('📦 单组产出'))
    chk('与 JSON kpi 段一致（转化率）', abs(e['conv'] - K['conv']['rate']) < 0.01, '%s vs %s' % (round(e['conv'], 2), K['conv']['rate']))
    chk('与 JSON kpi 段一致（沉睡）', e['sleep'] == K['sleep']['n'], '%s vs %s' % (e['sleep'], K['sleep']['n']))
    chk('与 JSON kpi 段一致（复购）', e['rep'] == K['repurchase']['n'], '%s vs %s' % (e['rep'], K['repurchase']['n']))
    chk('与 JSON kpi 段一致（同比）', abs(e['yoy'] - K['amt']['yoy_growth']) < 0.01, '%s vs %s' % (round(e['yoy'], 2), K['amt']['yoy_growth']))
    chk('口径说明含「同期」', '同期' in pg.evaluate("() => document.querySelector('main').innerText"))
    chk('说明含赠品占比 83.6%', '83.6' in pg.evaluate("() => document.querySelector('main').innerText"))
    rows = pg.evaluate("() => document.querySelectorAll('.data-table tbody tr').length")
    chk('沉睡清单 TOP20', rows == min(20, e['sleep']), str(rows))
    body = pg.evaluate("() => document.querySelector('.data-table tbody').innerText")
    chk('清单首位 = 戴春艳课题组（409 天）', '戴春艳课题组' in body.split('\n')[0] and '409' in body)
    pg.screenshot(path='/tmp/ls_kpi_admin.png', full_page=True)

    print('\n③ 销售视角：只按本人复算（应≠全量）')
    boot(pg, '黄建华', 'sales', 'ls')
    e2 = expect('黄建华')
    c2 = cards(pg)
    print('    黄建华期望：转化率 %.2f%% ｜ 成交额 %.2f 万 ｜ 复购 %d' % (e2['conv'], e2['amt'] / 1e4, e2['rep']))
    print('    页面卡片：', c2)
    chk('转化率按本人 %.2f%%' % e2['conv'], c2.get('🎯 测试→成交转化率', '').startswith('%.2f' % e2['conv']), c2.get('🎯 测试→成交转化率'))
    chk('成交额按本人 %.2f万' % (e2['amt'] / 1e4), c2.get('💵 累计成交额', '').startswith('%.2f' % (e2['amt'] / 1e4)), c2.get('💵 累计成交额'))
    chk('复购按本人 %d' % e2['rep'], c2.get('🔁 复购课题组') == str(e2['rep']), c2.get('🔁 复购课题组'))
    chk('≠ 全量成交额（隔离生效）', abs(e2['amt'] - e['amt']) > 1, '%.0f vs %.0f' % (e2['amt'], e['amt']))
    chk('标注「仅本人」', '仅本人' in pg.evaluate("() => document.querySelector('main').innerText"))
    chk('0 JS 错误', len(errs) == 0, str(errs[:2]))
    br.close()

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