#!/usr/bin/env python3
"""تست‌های سرتاسری فرانت‌اند با Playwright + سرور ساختگی.
اجرا:  pip install playwright && playwright install chromium && python3 tests/e2e.py"""
import subprocess, sys, time, json, base64, urllib.request, os
from playwright.sync_api import sync_playwright
HERE = os.path.dirname(os.path.abspath(__file__))
procs = [subprocess.Popen([sys.executable, os.path.join(HERE, 'mock_api.py'), '8777']),
         subprocess.Popen([sys.executable, os.path.join(HERE, 'mock_api.py'), '8778', '--noapi'])]
time.sleep(1.2)
res = []
def ok(name, cond, extra=''):
    res.append(bool(cond)); print(('PASS ' if cond else 'FAIL ') + name + (('  — ' + str(extra)) if extra and not cond else ''))
def get(path, port=8777): return json.load(urllib.request.urlopen('http://127.0.0.1:%d%s' % (port, path)))
PUB = "window.BK_SITE={devHosts:[]}"
mp = base64.urlsafe_b64encode(json.dumps({"f": "علی", "l": "رضایی", "p": "0912"}, ensure_ascii=False).encode()).decode().rstrip('=')
try:
    with sync_playwright() as p:
        b = p.chromium.launch()
        def page(init=None, errs=None):
            pg = b.new_page()
            if init: pg.add_init_script(init)
            if errs is not None: pg.on('pageerror', lambda e: errs.append(str(e)[:120]))
            return pg
        # 1) سایت عمومی: پنل مدیر پنهان و غیرفعال
        get('/_reset'); errs = []
        pg = page(PUB, errs); pg.goto('http://127.0.0.1:8777/index.html'); pg.wait_for_timeout(3500)
        st = pg.evaluate("()=>({g:getComputedStyle(document.getElementById('adminSettingsBtn')).display,o:getComputedStyle(document.getElementById('adminOverlay')).display,v:window.BK_isVerifiedAdmin()})")
        ok('عمومی: دکمه و پنل مدیر پنهان', st['g'] == 'none' and st['o'] == 'none' and st['v'] is False, st)
        tabs = {x[1] for x in get('/_log') if x[0] == 'query'}
        ok('عمومی: کوئری ویژهٔ مدیر ارسال نمی‌شود', not (tabs & {'bk_invoice_messages', 'orders', 'marketer_orders', 'bk_telegram_settings'}), tabs)
        ok('عمومی: بدون خطای اسکریپت', not errs, errs)
        dups = pg.evaluate("()=>{const c={};document.querySelectorAll('[id]').forEach(e=>c[e.id]=(c[e.id]||0)+1);return Object.keys(c).filter(k=>c[k]>1)}")
        ok('عمومی: idهای تکراری در DOM نیست', not dups, dups)
        vp = pg.evaluate("()=>document.querySelector('meta[name=viewport]').content")
        ok('عمومی: viewport اجازهٔ زوم می‌دهد', 'user-scalable=no' not in vp and 'maximum-scale' not in vp, vp)
        # 2) لینک بازاریاب روی دستگاه بدون رجیستری
        pg2 = page(PUB); pg2.goto('http://127.0.0.1:8777/index.html?marketer=mkabc123def456ghi789&mp=' + mp); pg2.wait_for_timeout(2500)
        ok('بازاریاب: ورود با لینک روی دستگاه جدید', pg2.evaluate("sessionStorage.getItem('bk-active-marketer-token')") == 'mkabc123def456ghi789')
        # 3) دروازهٔ ورود مدیر
        get('/_reset'); errs = []
        ad = page(None, errs); ad.goto('http://admin.localhost:8777/index.html'); ad.wait_for_timeout(3500)
        ok('ادمین: بدون نشست، دروازهٔ ورود نمایش داده می‌شود', ad.is_visible('#bkGate') and ad.is_visible('#bkgEmail'))
        ok('ادمین: پنل قبل از ورود باز نیست', not ad.evaluate("document.getElementById('adminOverlay').classList.contains('open')"))
        ad.fill('#bkgEmail', 'admin@x'); ad.fill('#bkgPass', 'bad'); ad.click('#bkgGo'); ad.wait_for_timeout(700)
        ok('ادمین: رمز اشتباه پیام خطا می‌دهد', 'ناموفق' in ad.inner_text('#bkgMsg'))
        ad.fill('#bkgEmail', 'viewer@x'); ad.fill('#bkgPass', 'good'); ad.click('#bkgGo'); ad.wait_for_timeout(900)
        ok('ادمین: حساب غیرمدیر رد می‌شود', 'دسترسی' in ad.inner_text('#bkgMsg') and ad.is_visible('#bkgOut'))
        ad.click('#bkgOut'); ad.wait_for_timeout(500)
        ad.fill('#bkgEmail', 'admin@x'); ad.fill('#bkgPass', 'good'); ad.click('#bkgGo'); ad.wait_for_timeout(4500)
        ok('ادمین: با حساب مدیر پنل باز می‌شود', ad.evaluate("document.getElementById('adminOverlay').classList.contains('open') && window.BK_isVerifiedAdmin()"))
        ok('ادمین: بدون خطای اسکریپت', not errs, errs)
        # 4) همگام‌سازی فاکتورها (۷۰۰ ردیف سرور، ۳۰۰ جدیدترین محلی)
        ids = ad.evaluate("()=>JSON.parse(localStorage.getItem('bk-group-messages-v1')||'[]').map(m=>m.id)")
        ok('فاکتورها: جدیدترین‌ها رسیدند (r700) و حد ۳۰۰ رعایت شد', 'r700' in ids and len(ids) == 300, (len(ids), 'r700' in ids))
        n0 = len([x for x in get('/_log') if x[0] == 'query' and x[1] == 'bk_invoice_messages' and x[2] == '*'])
        ad.evaluate("()=>window.BK_syncRemoteInvoiceMessages()"); ad.wait_for_timeout(1200)
        n1 = len([x for x in get('/_log') if x[0] == 'query' and x[1] == 'bk_invoice_messages' and x[2] == '*'])
        ok('فاکتورها: همگام‌سازی دوم دانلود اضافه ندارد', n1 == n0, (n0, n1))
        # 5) حذف فاکتور از کل سیستم
        ad.evaluate("()=>{window.BK_selectAdminTab('dashboard');document.getElementById('adminDirectBtn').click()}"); ad.wait_for_timeout(1500)
        key = ad.evaluate("()=>{const b=document.querySelector('[data-delete-group]');return b&&b.dataset.deleteGroup}")
        ok('حذف: دکمهٔ حذف روی کارت فاکتور هست', bool(key))
        if key:
            ad.once('dialog', lambda d: d.accept())
            ad.evaluate("(k)=>window.BK_deleteInvoiceGroup(k)", key); ad.wait_for_timeout(2000)
            dels = [x for x in get('/_log') if x[0] == 'mutate' and x[1] == 'bk_invoice_messages' and x[2] == 'delete']
            left = ad.evaluate("()=>JSON.parse(localStorage.getItem('bk-group-messages-v1')||'[]').length")
            ok('حذف: از سرور و حافظهٔ محلی پاک می‌شود', len(dels) >= 1 and left < 300, (len(dels), left))
        # 6) تنظیمات مشترک: مدیر ذخیره می‌کند، دستگاه عمومی می‌خواند
        ad.evaluate("()=>{localStorage.setItem('bk_invoice_style_v1', JSON.stringify({colorAccent:'#123456'})); return window.BK_SETTINGS.push('bk_invoice_style_v1')}"); ad.wait_for_timeout(1000)
        ok('تنظیمات: روی سرور ذخیره شد', 'bk_invoice_style_v1' in get('/_settings'))
        pub = page(PUB); pub.goto('http://127.0.0.1:8777/index.html'); pub.wait_for_timeout(4500)
        got = pub.evaluate("localStorage.getItem('bk_invoice_style_v1')")
        ok('تنظیمات: دستگاه دیگر سبک فاکتور مدیر را دریافت کرد', got and '#123456' in got, got)
        # 7) بدون API: حالت محلی
        no = page(None); no.goto('http://admin.localhost:8778/index.html'); no.wait_for_timeout(4000)
        ok('بدون سرور: گزینهٔ «حالت محلی» نمایش داده می‌شود', no.is_visible('#bkgLocal'))
        no.click('#bkgLocal'); no.wait_for_timeout(1500)
        ok('بدون سرور: حالت محلی پنل را باز می‌کند', no.evaluate("document.getElementById('adminOverlay').classList.contains('open')"))
        # 8) ساعت سرور مرجع تاریخ است (ساعت دستگاه ۷ روز عقب)
        sk = page("(function(){var o=Date.now;Date.now=function(){return o()-7*86400000}})()"); sk.goto('http://127.0.0.1:8777/index.html'); sk.wait_for_timeout(3000)
        diff = sk.evaluate("()=>Math.abs((window.BK_serverNow()) - (%d))" % int(time.time() * 1000 + 7 * 86400000 * 0))
        # BK_serverNow = Date.now(skewed)+offset ≈ زمان واقعی سرور
        ok('ساعت: BK_serverNow با ساعت واقعی سرور هماهنگ است', diff < 8000, diff)
        # 9) چاپ: بین فاکتورها شکست صفحه
        pr = page(None); pr.goto('http://localhost:8777/index.html'); pr.wait_for_timeout(2500)
        pr.emulate_media(media='print')
        v = pr.evaluate("""()=>{const a=document.getElementById('printInvoiceArea');document.documentElement.classList.add('bk-printing');
          a.innerHTML='<div class="pinv-imgpage"><img alt=""></div><div class="pinv-imgpage"><img alt=""></div><div class="pinv-imgpage"><img alt=""></div>';
          return [getComputedStyle(a.children[1]).breakBefore, getComputedStyle(a.children[2]).breakBefore, getComputedStyle(a.children[0]).breakBefore]}""")
        ok('چاپ: هر فاکتور روی برگ جدا (break-before)', v[0] == 'page' and v[1] == 'page' and v[2] != 'page', v)
        b.close()
finally:
    for pr_ in procs: pr_.terminate()
print('\n%d/%d تست موفق' % (sum(res), len(res)))
sys.exit(0 if all(res) else 1)
