import re, sys, json, subprocess, base64
from playwright.sync_api import sync_playwright
BASE = 'http://127.0.0.1:8090/'
R = []
def ok(c, n, x=''): R.append(bool(c)); print(('  ✓ ' if c else '  ✗ ') + n + ('' if c else ' ' + str(x)[:250]))
def sql(q): return subprocess.run(['mysql', '-uroot', 'agent_tracker', '-N', '-e', q], capture_output=True, text=True).stdout.strip()
SH = '/tmp/t-'
def login(ctx):
    p = ctx.new_page(); errs = []
    p.on('pageerror', lambda e: errs.append(str(e)))
    p.on('console', lambda m: errs.append(m.text) if m.type == 'error' and 'ERR_NAME_NOT_RESOLVED' not in m.text else None)
    p.on('dialog', lambda d: d.accept('https://summit.example.com'))
    p.goto(BASE + 'login.html'); p.fill('input[type=email]', 'chris@example.com'); p.fill('input[type=password]', 'Sup3r-Secret-99')
    p.click('#go'); p.wait_for_selector('#sidebar .sb-brand', timeout=15000); return p, errs
def noscroll(p): return p.evaluate('document.documentElement.scrollWidth <= innerWidth + 1')
aid = sql("SELECT id FROM agents WHERE full_name='Alex Rivera'")

with sync_playwright() as pw:
    b = pw.chromium.launch()
    for label, (W, H, mob) in {'desktop': (1440, 900, False), 'phone': (390, 844, True)}.items():
        print('== ' + label)
        sql("UPDATE workspaces SET settings=JSON_SET(settings,'$.brand.setup_done',false) WHERE id=1")
        ctx = b.new_context(viewport={'width': W, 'height': H}, is_mobile=mob, has_touch=mob)
        p, errs = login(ctx)
        # ---- set up the agency before the first invite
        if mob: p.click('#sbOpen, .mob-menu, [aria-label="Open menu"]', timeout=3000) if p.query_selector('#sbOpen, .mob-menu, [aria-label="Open menu"]') else None
        p.goto(BASE + '#/team'); p.wait_for_timeout(1500)
        p.evaluate("PH.addAgent()"); p.wait_for_selector('#wsuOk')
        ok(p.is_visible('text=First, set up your agency'), 'adding an agent asks for the agency first')
        p.screenshot(path=SH + label + '-setup.png')
        p.fill('#wsuName', 'Summit Financial Group' if label == 'desktop' else 'Summit Financial Group')
        p.click('[data-preset="#2563eb|#0f1e3d"]')
        ok('#2563eb' in p.inner_html('#wsuPv').lower() or 'rgb(37, 99, 235)' in p.inner_html('#wsuPv'), 'preview follows the colors picked')
        p.click('#wsuOk'); p.wait_for_selector('text=Add an agent', timeout=8000)
        ok(True, 'then the add-agent window opens')
        ok(p.evaluate("getComputedStyle(document.documentElement).getPropertyValue('--gold').trim()") == '#2563eb', 'brand color applied to the app')
        ok(p.evaluate("getComputedStyle(document.documentElement).getPropertyValue('--on-gold').trim()") == '#ffffff', 'white text on a blue accent')
        ok('Summit Financial Group' in p.inner_text('#sidebar .sb-brand') and 'PropHog' not in p.inner_text('#sidebar'), 'sidebar shows the agency, no PropHog')
        ok(p.title().endswith('Summit Financial Group'), 'browser tab carries the agency', p.title())
        p.keyboard.press('Escape'); p.wait_for_timeout(300)
        # ---- brand page
        p.goto(BASE + '#/settings/brand'); p.wait_for_selector('.wl-brand')
        ok(not p.is_enabled('#wlSave'), 'nothing to save until something changes')
        p.click('[data-preset="#10a37f|#0c2b25"]'); ok(p.is_enabled('#wlSave') and p.is_enabled('#wlUndo'), 'a change enables Save and Undo')
        p.click('#wlUndo'); ok(not p.is_enabled('#wlSave'), 'Undo puts it back')
        p.fill('[data-key=tool_name]', 'Summit Back Office' if label == 'desktop' else 'Summit Office')
        png = 'tests/white-label/logo.png'
        with p.expect_file_chooser() as fc: p.click('[data-pick=logo]')
        fc.value.set_files(png); p.wait_for_selector('.wl-logo[data-slot=logo] img', timeout=8000)
        ok(True, 'logo uploads and shows')
        p.click('#wlSave'); p.wait_for_timeout(1500)
        ok(('Summit Back Office' if label == 'desktop' else 'Summit Office') .lower() in p.inner_text('#sidebar .sb-brand').lower() and p.query_selector('#sidebar .sb-brand img'), 'saved: tool name and logo in the menu')
        ok(noscroll(p), 'brand page: no sideways scroll'); p.screenshot(path=SH + label + '-brand.png', full_page=True)
        # ---- email designer
        p.goto(BASE + '#/settings/emails'); p.wait_for_selector('#edFrame')
        p.wait_for_function("document.querySelector('#edFrame').srcdoc.includes('Welcome to Summit Financial Group')", timeout=10000)
        ok(True, 'preview renders the invite with the agency name')
        p.click('[data-toggle="1"]') if not p.query_selector('.ed-blk.open [data-k=text]') else None
        p.fill('.ed-blk.open [data-k=text]', 'Hey {{first_name}}, your seat at {{workspace}} is ready')
        p.wait_for_function("document.querySelector('#edFrame').srcdoc.includes('Hey Taylor, your seat at Summit Financial Group is ready')", timeout=8000)
        ok(True, 'typing updates the live preview with sample values')
        n0 = p.eval_on_selector_all('.ed-blk', 'e => e.length')
        p.click('[data-add=image]'); ok(p.eval_on_selector_all('.ed-blk', 'e => e.length') == n0 + 1, 'add a banner block')
        with p.expect_file_chooser() as fc: p.click('.ed-blk.open [data-img]')
        fc.value.set_files(png); p.wait_for_selector('.ed-blk.open [data-img] img', timeout=8000)
        p.wait_for_function("document.querySelector('#edFrame').srcdoc.includes('/api/media/')", timeout=8000); ok(True, 'banner upload shows in the preview')
        p.click('.ed-blk.open [data-del]'); ok(p.eval_on_selector_all('.ed-blk', 'e => e.length') == n0, 'delete a block')
        p.click('#edUndo'); ok(p.eval_on_selector_all('.ed-blk', 'e => e.length') == n0 + 1, 'Undo brings it back')
        p.click('[data-up="2"]'); ok(True, 'reorder with the arrows')
        p.click('[data-view=mobile]'); p.wait_for_timeout(400)
        w = p.eval_on_selector('#edFrame', 'f => f.style.width'); ok(w == '375px', 'mobile preview is a 375px phone', w)
        p.screenshot(path=SH + label + '-emails-mobile.png')
        p.click('[data-view=desktop]'); p.wait_for_timeout(300); ok(p.eval_on_selector('#edFrame', 'f => f.style.width') == '680px', 'desktop preview is full email width')
        p.click('[data-email=welcome]'); p.wait_for_selector('text=Leave without saving?'); p.click('text=Cancel'); ok(p.is_visible('[data-email=invite].on'), 'switching away with unsaved work asks first')
        p.click('#edSave'); p.wait_for_function("document.querySelector('#edSave').textContent.trim() === 'Saved'", timeout=8000)
        ok(p.query_selector('[data-email=invite] i'), 'saved — marked as customized')
        ok(sql("SELECT COUNT(*) FROM email_templates WHERE workspace_id=1 AND tkey='invite' AND design IS NOT NULL") == '1', 'design stored')
        p.click('#edTest'); p.wait_for_selector('text=Test sent to chris@example.com', timeout=8000); ok(True, 'send me a test')
        ok(noscroll(p), 'email designer: no sideways scroll'); p.screenshot(path=SH + label + '-emails.png')
        p.click('#edReset'); p.click('text=Restore original'); p.wait_for_timeout(1200); ok(not p.query_selector('[data-email=invite] i'), 'restore the original')
        # ---- the real invite uses it
        p.goto(BASE + '#/settings/members'); p.wait_for_selector('#invSend')
        p.fill('[data-key=email]', label + '.newagent@example.com'); p.click('#invSend'); p.wait_for_timeout(1500)
        log = open('storage/logs/mail.log').read().split('---')[-2]
        ok('Welcome to Summit Financial Group' in log and 'PropHog' not in log, 'invite email goes out as the agency', log[-200:])
        tok = re.search(r'invite\.html\?token=([A-Za-z0-9_-]+)', log).group(1)
        # ---- pipeline CRM
        p.goto(BASE + '#/agents/' + aid + '/pipeline?p=recruits'); p.wait_for_selector('#pipePick')
        ok(p.eval_on_selector('#pipePick', 's => s.tagName') == 'SELECT' and not p.query_selector('.pipe-tabs'), 'pipelines switch from a dropdown, no pill row')
        ok(not p.query_selector('text=No phone') and not p.query_selector('text=No email'), 'cards show data or nothing')
        p.select_option('#pipePick', 'top100'); p.wait_for_function("document.querySelector('#pipeSum') && /Top 100|top 100/i.test(document.querySelector('#pipeQ').placeholder)", timeout=8000); ok(True, 'switch pipeline')
        p.wait_for_selector('.pcard.crm', timeout=10000); p.wait_for_timeout(400)
        card = p.query_selector('.pcard.crm'); name = card.query_selector('.nm b').inner_text(); card.click(); p.wait_for_selector('.rec2-h')
        ok(p.is_visible('#recStage') and p.is_visible('text=Call'), 'record: stage in the header, Call / Text / Email')
        if mob: p.click('[data-rt=activity]')
        if p.query_selector('.fu-bar.set [data-fu=edit]'): p.click('.fu-bar.set [data-fu=edit]'); p.wait_for_timeout(300)
        p.click('[data-pre=tomorrow]'); p.fill('#fuNote', 'Call after work about the interview'); p.click('[data-fu=save]')
        p.wait_for_selector('.fu-bar.set', timeout=8000); ok('Call after work' in p.inner_text('.fu-bar.set'), 'follow-up set: Tomorrow + note')
        p.click('[data-kind=call]'); p.fill('#nBody', 'No answer, left a voicemail'); p.keyboard.press('Control+Enter')
        p.wait_for_selector('.tl-i.k-call', timeout=8000); ok('voicemail' in p.inner_text('.tl-i.k-call'), 'call logged with Ctrl+Enter')
        p.click('.tl-i.k-call [data-nedit]'); p.fill('.tl-edit', 'No answer, left a voicemail — try Thursday'); p.click('[data-ne=save]'); p.wait_for_selector('text=· edited', timeout=6000); ok(True, 'edit a logged entry')
        p.click('[data-rt=details]') if mob else None
        p.fill('[data-key=occupation]', 'Teacher'); p.wait_for_timeout(1300)
        ok(sql(f"SELECT JSON_UNQUOTE(JSON_EXTRACT(fields,'$.occupation')) FROM cards WHERE name='{name}' LIMIT 1") == 'Teacher', 'details save as you type')
        p.click('[data-rt=activity]') if mob else None
        p.screenshot(path=SH + label + '-record.png')
        ok(noscroll(p), 'record: no sideways scroll')
        st2 = p.eval_on_selector('#recStage', 's => s.options[1].value'); p.select_option('#recStage', st2); p.wait_for_timeout(1500)
        ok(sql(f"SELECT s.sort_order FROM cards c JOIN pipeline_stages s ON s.id=c.stage_id WHERE c.name='{name}' LIMIT 1") == '1', 'stage changed from the header')
        p.keyboard.press('Escape'); p.wait_for_timeout(500)
        ok(p.query_selector('.fu-strip [data-due]') is not None, 'follow-up strip appears above the board')
        p.click('.fu-strip [data-due=week], .fu-strip [data-due=today]'); p.wait_for_timeout(400)
        if mob:
            ok(p.eval_on_selector_all('.pipe-acc .pcard', 'e => e.length') <= 1, 'strip filters to the lead that is due')
        else:
            ok(p.eval_on_selector_all('.pcard.crm', 'e => e.length') == int(p.inner_text('.fu-chip.on b')) and p.query_selector('.pcard.crm .due'), 'strip filters the board to what is due; card carries the badge')
        p.click('.fu-strip [data-due=""]'); ok(p.eval_on_selector_all('.pcard.crm', 'e => e.length') > 1, 'Show everyone clears it')
        p.screenshot(path=SH + label + '-board.png')
        # move to another pipeline from the record
        p.query_selector('.pcard.crm').click(); p.wait_for_selector('#recStage')
        tid = p.eval_on_selector('#recStage', "s => [...s.querySelectorAll('optgroup')][1].querySelector('option').value")
        nm2 = p.inner_text('#recName'); p.select_option('#recStage', tid); p.wait_for_timeout(1500)
        ok(sql(f"SELECT p.pkey FROM cards c JOIN pipelines p ON p.id=c.pipeline_id WHERE c.name='{nm2}' AND c.agent_id={aid} ORDER BY c.updated_at DESC LIMIT 1") != 'top100', 'move to another pipeline from the stage dropdown')
        # overview follow-ups (leaderboard is gone)
        p.goto(BASE + '#/agents/' + aid + '/overview'); p.wait_for_selector('#fuPanel:not([hidden]) .fu-row', timeout=10000)
        ok(not p.query_selector('text=Team leaderboard'), 'no leaderboard on Overview')
        ok(name in p.inner_text('#fuPanel'), 'Overview lists the follow-up')
        p.locator('#fuPanel .fu-row', has_text=name).first.click(); p.wait_for_selector('.rec2-h', timeout=10000); ok(name in p.inner_text('#recName'), 'clicking it opens that lead')
        p.keyboard.press('Escape')
        p.goto(BASE + '#/team'); p.wait_for_timeout(1500); ok(not p.query_selector('text=Team leaderboard') and not p.query_selector('#teamLb'), 'no leaderboard on All agents')
        ok(not [e for e in errs], 'no script errors', errs[:3])
        # ---- signed-out pages carry the brand
        p.evaluate("PH.logout()"); p.wait_for_url('**/login.html', timeout=8000); p.wait_for_timeout(500)
        t = p.inner_text('body'); ok('Summit Financial Group' in t and 'PropHog' not in t, 'sign-in page shows the agency (this device)')
        ok(noscroll(p), 'sign-in: no sideways scroll'); p.screenshot(path=SH + label + '-login.png')
        p2 = b.new_context(viewport={'width': W, 'height': H}, is_mobile=mob).new_page()
        p2.goto(BASE + 'invite.html?token=' + tok); p2.wait_for_timeout(1500)
        t = p2.inner_text('body'); ok('Summit Financial Group' in t and 'PropHog' not in t and p2.query_selector('[data-brand] img'), 'fresh browser: invite page shows the agency logo and name')
        p2.screenshot(path=SH + label + '-invite.png')
        ctx.close()
    b.close()
print(f"\n{sum(R)} passed, {len(R) - sum(R)} failed"); sys.exit(0 if all(R) else 1)
