#!/usr/bin/env python3
from pathlib import Path
import sys
root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).resolve().parents[1]
checks = {
    'inbox ListView virtualization': ('src/com/lemenzo/messenger/MainActivity.java', 'new ListView(this)'),
    'inbox ViewHolder recycling': ('src/com/lemenzo/messenger/MainActivity.java', 'ConversationRowHolder'),
    'inbox convertView reuse': ('src/com/lemenzo/messenger/MainActivity.java', 'convertView.getTag() instanceof ConversationRowHolder'),
    'timeline ListView virtualization': ('src/com/lemenzo/messenger/ConversationActivity.java', 'new ListView(this)'),
    'timeline message ViewHolder recycling': ('src/com/lemenzo/messenger/ConversationActivity.java', 'MessageRowHolder'),
    'timeline pending ViewHolder recycling': ('src/com/lemenzo/messenger/ConversationActivity.java', 'PendingRowHolder'),
    'message paging': ('src/com/lemenzo/messenger/data/MessageRepository.java', 'messagesPage('),
    'stable SMS date+id cursor': ('src/com/lemenzo/messenger/data/MessageRepository.java', 'Telephony.Sms._ID + "<?))"'),
    'stable MMS date+id cursor': ('src/com/lemenzo/messenger/data/MessageRepository.java', 'Telephony.Mms._ID + "<?))"'),
    'per-protocol continuation cursor': ('src/com/lemenzo/messenger/data/MessagePage.java', 'nextSmsId'),
    '300-message initial page': ('src/com/lemenzo/messenger/ConversationActivity.java', 'MESSAGE_PAGE_SIZE = 300'),
    'older-history loader': ('src/com/lemenzo/messenger/ConversationActivity.java', 'loadOlderMessages()'),
    'history-preserving provider merge': ('src/com/lemenzo/messenger/ConversationActivity.java', 'mergeNewestPageLocked'),
    'batch contact prefetch': ('src/com/lemenzo/messenger/data/ContactResolver.java', 'prefetchPresentations('),
    'contact prefetch cache guard': ('src/com/lemenzo/messenger/data/ContactResolver.java', 'nameCache.containsKey(number) && photoCache.containsKey(number)'),
    'thumbnail LRU cache': ('src/com/lemenzo/messenger/ConversationActivity.java', 'LruCache<String, android.graphics.Bitmap>'),
    'refresh coalescing': ('src/com/lemenzo/messenger/MainActivity.java', 'refreshInFlight'),
    'swipe direction lock': ('src/com/lemenzo/messenger/ui/SwipeDeleteTouchListener.java', 'HORIZONTAL_BIAS'),
}
failed=[]
for name,(rel,needle) in checks.items():
    p=root/rel
    if not p.exists() or needle not in p.read_text(errors='ignore'):
        failed.append(name)

# Old date-only/window-reload patterns must stay gone.
for rel, forbidden in [
    ('src/com/lemenzo/messenger/ConversationActivity.java', 'loadedWindowTarget'),
    ('src/com/lemenzo/messenger/ConversationActivity.java', 'oldestLoadedDate'),
    ('src/com/lemenzo/messenger/MainActivity.java', 'return row(getItem(position))'),
    ('src/com/lemenzo/messenger/ConversationActivity.java', 'return buildMessageRow(item.message,parent)'),
]:
    p=root/rel
    if p.exists() and forbidden in p.read_text(errors='ignore'):
        failed.append('forbidden legacy pattern: '+forbidden)

if failed:
    print('PERFORMANCE VALIDATION: FAIL')
    for x in failed: print('-',x)
    raise SystemExit(1)
print('PERFORMANCE VALIDATION: PASS')
for x in checks: print('-',x)


# Regression model for the keyset cursor. This intentionally creates more than one page
# of SMS and MMS rows with exactly the same timestamp. A date-only or cursor-reset bug
# would either duplicate rows forever or stop before all rows are seen.
MAX = (1 << 63) - 1
rows = []
for i in range(1, 901): rows.append((0, i, 1700000000000))
for i in range(1, 701): rows.append((1, i, 1700000000000))
for i in range(901, 1201): rows.append((0, i, 1699999000000 - (i % 17)))
for i in range(701, 1001): rows.append((1, i, (1699998000 - (i % 7)) * 1000))

def model_page(before_date, sms_id, mms_id, limit=300):
    def eligible(row):
        protocol, row_id, date = row
        if before_date == MAX:
            return True
        if protocol == 0:
            return date < before_date or (date == before_date and row_id < sms_id)
        second = before_date // 1000
        raw_second = date // 1000
        if before_date % 1000 == 0:
            return raw_second < second or (raw_second == second and row_id < mms_id)
        return raw_second <= second
    candidates = [r for r in rows if eligible(r)]
    candidates.sort(key=lambda r: (-r[2], r[0], -r[1]))
    out = candidates[:limit]
    if not out:
        return [], False, (MAX, MAX, MAX)
    next_date = out[-1][2]
    next_sms = sms_id if next_date == before_date else MAX
    next_mms = mms_id if next_date == before_date else MAX
    for protocol, row_id, date in out:
        if date != next_date:
            continue
        if protocol == 0:
            next_sms = min(next_sms, row_id)
        else:
            next_mms = min(next_mms, row_id)
    return out, len(candidates) > limit, (next_date, next_sms, next_mms)

cursor = (MAX, MAX, MAX)
seen = []
for _ in range(50):
    page, more, cursor = model_page(*cursor)
    if not page:
        break
    seen.extend(page)
    if not more:
        break
assert len(seen) == len(set((p, i) for p, i, _ in seen)), 'keyset cursor produced duplicates'
assert set((p, i) for p, i, _ in seen) == set((p, i) for p, i, _ in rows), 'keyset cursor skipped rows'
print('- equal-timestamp keyset paging regression')

# v4.9: expensive compose/contact/attachment/send work must stay off the UI thread.
compose = (root / "src/com/lemenzo/messenger/ComposeActivity.java").read_text()
conversation = (root / "src/com/lemenzo/messenger/ConversationActivity.java").read_text()
extra_checks = {
    "Compose IO executor": "ioExecutor.execute" in compose,
    "contact picker dedicated screen": (root / "src/com/lemenzo/messenger/ContactPickerActivity.java").exists(),
    "contact picker off UI thread": "io.execute(() ->" in (root / "src/com/lemenzo/messenger/ContactPickerActivity.java").read_text(),
    "contact picker deduplicates contacts": "Set<Long> seen" in (root / "src/com/lemenzo/messenger/ContactPickerActivity.java").read_text(),
    "contact picker deduplicates phone numbers": "normalizedSeen" in (root / "src/com/lemenzo/messenger/ContactPickerActivity.java").read_text(),
    "compose attachment thumbnail decode": "loadThumbnail" in compose,
    "compose send off UI thread": "sendExecutor.execute" in compose and "MmsSender.send(getApplicationContext()" in compose,
    "conversation attachment thumbnail off UI thread": "mediaExecutor.execute" in conversation and "loadThumbnail" in conversation,
    "conversation send off UI thread": "sendExecutor.execute" in conversation and "MmsSender.send(getApplicationContext()" in conversation,
    "inbox avatar decode off UI thread": "avatarExecutor.execute" in (root / "src/com/lemenzo/messenger/MainActivity.java").read_text() and "loadThumbnail" in (root / "src/com/lemenzo/messenger/MainActivity.java").read_text(),
    "conversation header avatar off UI thread": "contact:" in conversation and "loadThumbnail" in conversation,
}
missing = [name for name, ok in extra_checks.items() if not ok]
if missing:
    print("PERFORMANCE VALIDATION: FAIL")
    for name in missing: print("-", name)
    raise SystemExit(1)
for name in extra_checks: print("-", name)

# v5.2 receiver-main-thread hardening gates.
receiver_async = (root/'src/com/lemenzo/messenger/util/ReceiverAsync.java').read_text()
receiver_files = [
    'src/com/lemenzo/messenger/sms/SmsDeliverReceiver.java',
    'src/com/lemenzo/messenger/sms/SendStatusReceiver.java',
    'src/com/lemenzo/messenger/notifications/NotificationActionReceiver.java',
    'src/com/lemenzo/messenger/mms/MmsWapPushDeliverReceiver.java',
    'src/com/lemenzo/messenger/mms/MmsDownloadResultReceiver.java',
    'src/com/lemenzo/messenger/mms/MmsSendResultReceiver.java',
]
async_errors = []
if 'goAsync()' not in receiver_async or 'ThreadPoolExecutor' not in receiver_async or 'ArrayBlockingQueue' not in receiver_async:
    async_errors.append('ReceiverAsync is not bounded/goAsync based')
for rel in receiver_files:
    text = (root/rel).read_text()
    if 'ReceiverAsync.run(this' not in text:
        async_errors.append(rel + ' does not offload follow-up receiver work')
if async_errors:
    print('PERFORMANCE VALIDATION: FAIL')
    for err in async_errors:
        print(' -', err)
    raise SystemExit(1)

print('- receiver callback offload regression')
