import json, base64, http.cookiejar, urllib.request, urllib.error, subprocess, re, sys
BASE = 'http://127.0.0.1:8090/api'
cj = http.cookiejar.CookieJar(); op = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
CSRF = ['']
R = []
def ok(c, n, x=''): R.append(bool(c)); print(('  ✓ ' if c else '  ✗ ') + n + ('' if c else ' ' + str(x)[:300]))
def call(m, path, body=None):
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(BASE + path, data=data, method=m, headers={'Content-Type': 'application/json', 'X-CSRF-Token': CSRF[0]})
    try:
        r = op.open(req); raw = r.read(); code = r.status
    except urllib.error.HTTPError as e:
        raw = e.read(); code = e.code
    try: j = json.loads(raw)
    except Exception: j = raw
    return code, j
def sql(q): return subprocess.run(['mysql', '-uroot', 'agent_tracker', '-N', '-e', q], capture_output=True, text=True).stdout.strip()

c, j = call('POST', '/auth/login', {'email': 'chris@example.com', 'password': 'Sup3r-Secret-99'}); CSRF[0] = j['csrf']
print('Schema'); 
ok(sql("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA='agent_tracker' AND COLUMN_NAME IN ('follow_up_at','follow_up_note','last_activity_at','edited_at','design')") == '5', 'new columns added on first request')
ok(sql("SHOW TABLES LIKE 'media'") == 'media', 'media table created')

print('Brand')
png = base64.b64encode(open('tests/white-label/logo.png', 'rb').read()).decode()
c, j = call('PUT', '/settings/brand', {'name': '  Summit   Financial Group ', 'tool_name': 'Summit Back Office', 'accent': '#0F6E5A', 'dark': '#0b2a24', 'logo': 'data:image/png;base64,' + png, 'from_name': 'Summit Financial Group', 'reply_to': 'Owner@Summit.com', 'setup_done': True})
ok(c == 200 and j['brand']['name'] == 'Summit Financial Group', 'agency name saved exactly as typed (no "Team" added)', j)
ok(j['brand']['logo'].startswith('http://127.0.0.1:8090/api/media/') and j['brand']['on_accent'] == '#ffffff', 'logo stored as a public image URL; white text chosen for a dark accent', j['brand'])
logo = j['brand']['logo']
r = urllib.request.urlopen(logo); ok(r.status == 200 and r.headers['Content-Type'] == 'image/png' and 'immutable' in r.headers['Cache-Control'], 'logo URL serves the image publicly (email clients can load it)')
c, j = call('PUT', '/settings/brand', {'accent': 'red; background:url(x)'}); ok(j['brand']['accent'] == '#f2c227', 'a bad colour falls back instead of being stored')
c, j = call('PUT', '/settings/brand', {'reply_to': 'not-an-email'}); ok(c == 422, 'bad reply-to refused')
c, j = call('PUT', '/settings/brand', {'accent': '#0F6E5A'})
c, j = call('POST', '/media', {'data': 'data:image/png;base64,' + base64.b64encode(b'<svg onload=alert(1)>').decode()}); ok(c == 422, 'non-image upload refused', j)
c, j = call('GET', '/workspace'); ok(j['brand']['tool_name'] == 'Summit Back Office', 'workspace bundle carries the brand')
c, j = call('GET', '/brand/public?ws=' + sql('SELECT slug FROM workspaces WHERE id=1')); ok(j['brand'] and j['brand']['name'] == 'Summit Financial Group' and 'reply_to' not in j['brand'], 'public brand for sign-in pages (no private fields)')

print('Email designer')
c, j = call('GET', '/settings/emails'); keys = [e['key'] for e in j['emails']]
ok(keys == ['invite', 'welcome', 'reset'], 'three designable emails', keys)
inv = [e for e in j['emails'] if e['key'] == 'invite'][0]; d = inv['design']
ok(not inv['custom'] and d['blocks'][0]['type'] == 'logo', 'invite starts from the default design')
c, j = call('POST', '/settings/emails/invite/preview', d)
ok(c == 200 and logo in j['html'] and 'Welcome to Summit Financial Group, Taylor' in j['html'] and '#0f6e5a' in j['html'].lower(), 'preview: logo, merge fields and brand colour', j.get('subject'))
ok('PropHog' not in j['html'] and 'PropHog' not in j['subject'], 'no PropHog anywhere in the email')
bad = json.loads(json.dumps(d)); bad['blocks'] = [b for b in bad['blocks'] if b['type'] != 'button']
c, j = call('PUT', '/settings/emails/invite', bad); ok(c == 422 and 'invite link' in j['error'], 'an invite without the invite link is refused', j)
x = json.loads(json.dumps(d))
x['subject'] = '{{first_name}}, your {{workspace}} seat is ready'
x['blocks'].insert(1, {'type': 'image', 'src': logo, 'alt': 'Banner', 'full': True})
x['blocks'][3]['html'] = '<p onclick="evil()">Hi <b>{{first_name}}</b> <script>alert(1)</script><a href="javascript:alert(1)">bad</a> <a href="https://summit.com">site</a></p>'
x['style']['font'] = 'georgia'
c, j = call('PUT', '/settings/emails/invite', x); ok(c == 200, 'custom invite saved', j)
saved = [e for e in j['emails'] if e['key'] == 'invite'][0]
h = saved['design']['blocks'][3]['html']
ok(saved['custom'] and 'onclick' not in h and 'script' not in h and 'javascript' not in h and '<strong>{{first_name}}</strong>' in h and 'href="https://summit.com"' in h, 'text is cleaned: scripts, handlers and javascript: links removed, formatting kept', h)
c, j = call('POST', '/settings/emails/invite/test', x); ok(c == 200, 'test send to me', j)
log = open('storage/logs/mail.log').read().split('---')[-2]
ok('[Test] Taylor, your Summit Financial Group seat is ready' in log, 'test email arrives with merged subject', log[-300:])

print('Real sends use the design')
c, j = call('POST', '/invites', {'email': 'jordan.lee@example.com', 'full_name': 'jordan lee', 'role': 'agent'})
ok(c == 200 and j['invite_link'], 'invite sent', j)
log = open('storage/logs/mail.log').read().split('---')[-2]
ok('Jordan, your Summit Financial Group seat is ready' in log and j['invite_link'] in log, 'the invite uses the custom subject and carries the real link')
c, j = call('DELETE', '/settings/emails/invite'); ok(not [e for e in j['emails'] if e['key'] == 'invite'][0]['custom'], 'reset to default')
c, j = call('POST', '/auth/forgot', {'email': 'chris@example.com'})
log = open('storage/logs/mail.log').read().split('---')[-2]
ok('Reset your Summit Financial Group password' in log and 'reset.html?token=' in log, 'password reset now uses its template (it was hard-coded before)', log[-300:])

print('Lead CRM')
aid = int(sql("SELECT id FROM agents WHERE full_name='Alex Rivera'"))
c, j = call('GET', f'/agents/{aid}/cards?pipeline=recruits'); card = j['cards'][0]; cid = card['id']
c, j = call('PUT', f'/cards/{cid}/follow-up', {'follow_up_at': '2026-10-20T14:00:00.000Z', 'follow_up_note': 'Call after work'})
ok(j['card']['follow_up_at'] == '2026-10-20 14:00:00' and j['card']['follow_up_note'] == 'Call after work', 'follow-up set (stored UTC)', j)
c, j = call('GET', f'/agents/{aid}/follow-ups'); ok(any(x['id'] == cid for x in j['cards']), 'shows in the agent\'s follow-ups')
c, j = call('PUT', f'/cards/{cid}/follow-up', {'follow_up_at': ''}); ok(j['card']['follow_up_at'] is None and j['card']['follow_up_note'] is None, 'follow-up cleared')
c, j = call('POST', f'/cards/{cid}/notes', {'kind': 'call', 'body': 'Left a voicemail'}); nid = j['note']['id']
ok(j['note']['kind'] == 'call', 'call logged as an activity')
c, j = call('PATCH', f'/cards/{cid}/notes/{nid}', {'body': 'Left a voicemail, will try Thursday'}); ok(j['note']['edited_at'] and 'Thursday' in j['note']['body'], 'note edited and marked edited')
mv = sql(f"SELECT id FROM card_notes WHERE card_id={cid} AND kind='move' LIMIT 1")
c, j = call('GET', '/workspace'); pipes = {p['pkey']: p for p in j['pipelines']}
dst = pipes['partners']['stages'][1]
c, j = call('POST', f'/cards/{cid}/transfer', {'pipeline': 'partners', 'stage_id': dst['id']})
ok(c == 200 and j['card']['pipeline_id'] == pipes['partners']['id'] and j['card']['stage_id'] == dst['id'], 'move to another pipeline works (route was missing)', j)
c, j = call('GET', f'/cards/{cid}/notes'); ok(j['notes'][0]['kind'] == 'move' and 'to Partners' in j['notes'][0]['body'] and any(n['id'] == nid for n in j['notes']), 'history travels with the lead')
mvid = j['notes'][0]['id']; c, j = call('PATCH', f'/cards/{cid}/notes/{mvid}', {'body': 'x'}); ok(c == 403, 'automatic entries are not editable')
print(f"\n{sum(R)} passed, {len(R) - sum(R)} failed"); sys.exit(0 if all(R) else 1)
