"""
test_cashout.py — Cashout Test Bot for betpawa.ug Aviator

Purpose: Test and debug the cashout button click mechanism.
  - Bets 1 UGX every round
  - Cashout target: >= 2x (hits frequently)
  - Detailed logging of every cashout attempt (selectors tried, JS fallback, etc.)
  - Logs balance before/after to verify cashout success

Usage:
    pip install playwright
    playwright install chromium
    python test_cashout.py
"""

import json
import re
import struct
import sys
import time
import random
import threading
from datetime import datetime
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout

# ═══════════════════════════════════════════════════════════
#  CONFIGURATION
# ═══════════════════════════════════════════════════════════

ACCOUNT = {"mobile": "785559778", "password": "Favour@2016"}
LOGIN_URL = "https://www.betpawa.ug/login"
AVIATOR_URL = "https://www.betpawa.ug/casino?gameId=aviator&filter=all"

STAKE = 1        # 1 UGX per bet
CASHOUT_TARGET = 2.0  # Cash out at >= 2x
MAX_ROUNDS = 50  # Stop after this many rounds


# ═══════════════════════════════════════════════════════════
#  LOGGING
# ═══════════════════════════════════════════════════════════

def log(msg, level="INFO"):
    ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
    tag = {"INFO": "INFO", "WARN": "WARN", "ERROR": "ERR!"}
    print(f"[{ts}] [{tag.get(level, level)}] {msg}", flush=True)


# ═══════════════════════════════════════════════════════════
#  GAME STATE (thread-safe)
# ═══════════════════════════════════════════════════════════

class GameState:
    def __init__(self):
        self._lock = threading.Lock()
        self._multiplier = 0.0
        self.crashed = False
        self.round_active = False
        self.betting_open = False
        self.ws_connected = False
        self.ws_msg_count = 0
        self.last_ws_time = time.time()
        self.crash_count = 0
        self.last_crash_value = 0.0
        self.bet_placed = False

    def get_multiplier(self):
        with self._lock:
            return self._multiplier

    def set_multiplier(self, val):
        with self._lock:
            self._multiplier = val

    def reset_round(self):
        with self._lock:
            self._multiplier = 0.0
        self.crashed = False
        self.round_active = False
        self.betting_open = False
        self.bet_placed = False


state = GameState()


# ═══════════════════════════════════════════════════════════
#  BINARY WEBSOCKET PROTOCOL DECODER (exact copy from cjavi.py)
# ═══════════════════════════════════════════════════════════

_PAT_X          = b'\x00\x01\x78\x07'
_PAT_CRASHX     = b'\x00\x06crashX\x07'
_PAT_STATE_ID   = b'\x00\x0anewStateId\x04'
_PAT_BALANCE    = b'\x00\x0anewBalance\x07'


def decode_game_binary(data):
    result = {}
    if not data or len(data) < 10:
        return result
    if data[0] != 0x80:
        return result

    idx = data.find(_PAT_X)
    if idx >= 0 and idx + 4 + 8 <= len(data):
        mult = struct.unpack('>d', data[idx + 4: idx + 12])[0]
        if 0.5 <= mult <= 100000.0:
            result['x'] = round(mult, 2)

    idx = data.find(_PAT_CRASHX)
    if idx >= 0 and idx + 9 + 8 <= len(data):
        crash = struct.unpack('>d', data[idx + 9: idx + 17])[0]
        if 1.0 <= crash <= 100000.0:
            result['crashX'] = round(crash, 2)

    idx = data.find(_PAT_STATE_ID)
    if idx >= 0 and idx + 12 + 4 <= len(data):
        state_id = struct.unpack('>I', data[idx + 12: idx + 16])[0]
        if state_id in (1, 2, 3):
            result['newStateId'] = state_id

    idx = data.find(_PAT_BALANCE)
    if idx >= 0 and idx + 12 + 8 <= len(data):
        bal = struct.unpack('>d', data[idx + 12: idx + 20])[0]
        result['newBalance'] = round(bal, 2)

    return result


def process_decoded_binary(decoded):
    if not decoded:
        return

    state.last_ws_time = time.time()

    if 'x' in decoded:
        state.set_multiplier(decoded['x'])
        state.round_active = True

    if 'crashX' in decoded:
        crash_val = decoded['crashX']
        state.crashed = True
        state.round_active = False
        state.betting_open = False
        state.crash_count += 1
        state.last_crash_value = crash_val
        log(f"  [WS] CRASH at {crash_val:.2f}x  (observed: {state.crash_count})")

    if 'newStateId' in decoded:
        sid = decoded['newStateId']
        if sid == 1:
            state.betting_open = True
            state.round_active = False
            state.set_multiplier(0.0)
        elif sid == 2:
            state.round_active = True
            state.betting_open = False


# ═══════════════════════════════════════════════════════════
#  SOCKJS / STOMP PARSER (exact copy from cjavi.py)
# ═══════════════════════════════════════════════════════════

def unwrap_sockjs(raw):
    if not raw:
        return []
    if raw == "o" or raw == "h":
        return []
    if raw.startswith("a["):
        try:
            return [m for m in json.loads(raw[1:]) if isinstance(m, str)]
        except json.JSONDecodeError:
            return []
    return [] if raw.startswith("c[") else [raw]


def parse_stomp_frame(raw):
    if not raw or "\n" not in raw:
        return None
    parts = raw.split("\n\n", 1)
    if len(parts) < 2:
        return None
    lines = parts[0].split("\n")
    command = lines[0].strip()
    headers = {}
    for line in lines[1:]:
        if ":" in line:
            k, v = line.split(":", 1)
            headers[k.strip()] = v.strip()
    return command, headers, parts[1].rstrip("\x00")


def _payload_to_string(payload):
    if isinstance(payload, str):
        return payload
    if isinstance(payload, bytes):
        try:
            return payload.decode("utf-8")
        except UnicodeDecodeError:
            return None
    if isinstance(payload, dict):
        inner = payload.get("payload", payload.get("data", ""))
        if isinstance(inner, str):
            return inner
        if isinstance(inner, bytes):
            try:
                return inner.decode("utf-8")
            except UnicodeDecodeError:
                return None
    return None


# ═══════════════════════════════════════════════════════════
#  WEBSOCKET INTERCEPTION (exact copy from cjavi.py)
# ═══════════════════════════════════════════════════════════

def setup_websocket_interception(page):
    def on_ws(ws):
        url = ws.url
        is_game = "bluebox" in url.lower()
        tag = "GAME" if is_game else "OTHER"
        log(f"  WebSocket [{tag}] connected: {url[:100]}...")
        if is_game:
            state.ws_connected = True

        def on_received(payload):
            state.ws_msg_count += 1
            state.last_ws_time = time.time()

            # Handle raw binary data first (game WS sends binary)
            raw_bytes = None
            if isinstance(payload, bytes):
                raw_bytes = payload
            elif isinstance(payload, dict):
                inner = payload.get("payload", payload.get("data", b""))
                if isinstance(inner, bytes):
                    raw_bytes = inner

            if raw_bytes is not None:
                if is_game:
                    decoded = decode_game_binary(raw_bytes)
                    if decoded:
                        process_decoded_binary(decoded)
                return

            # Handle text data (SockJS/STOMP)
            raw = _payload_to_string(payload)
            if not raw or raw in ("o", "h"):
                return
            for msg in unwrap_sockjs(raw):
                parse_stomp_frame(msg)

        def on_close():
            log(f"  WebSocket [{tag}] DISCONNECTED", "WARN")
            if is_game:
                state.ws_connected = False

        ws.on("framereceived", on_received)
        ws.on("close", on_close)

    page.on("websocket", on_ws)
    log("WebSocket interception enabled")


# ═══════════════════════════════════════════════════════════
#  HELPERS
# ═══════════════════════════════════════════════════════════

def human_delay(lo=0.15, hi=0.5):
    time.sleep(random.uniform(lo, hi))

def human_type(locator, text, lo_ms=50, hi_ms=140):
    locator.press_sequentially(text, delay=random.randint(lo_ms, hi_ms))


# ═══════════════════════════════════════════════════════════
#  LOGIN
# ═══════════════════════════════════════════════════════════

def login(page):
    mobile = ACCOUNT["mobile"]
    password = ACCOUNT["password"]
    log(f"Logging in as ***{mobile[-4:]}...")
    page.goto(LOGIN_URL, wait_until="domcontentloaded", timeout=60000)
    try:
        page.wait_for_load_state("load", timeout=20000)
    except Exception:
        pass
    human_delay(2, 4)

    mobile_selectors = [
        "input[type='tel']",
        "input[type='text'][inputmode='tel']",
        "input[name='phone']", "input[name='mobile']",
        "input[placeholder*='phone' i]", "input[placeholder*='mobile' i]",
        "input[placeholder*='number' i]", "form input[type='text']",
    ]
    mobile_input = None
    for attempt in range(3):
        for sel in mobile_selectors:
            try:
                loc = page.locator(sel).first
                if loc.count() > 0 and loc.is_visible(timeout=3000):
                    mobile_input = loc
                    break
            except Exception:
                continue
        if mobile_input:
            break
        human_delay(2, 4)
    if not mobile_input:
        raise RuntimeError("Could not find mobile input on login page.")

    mobile_input.click(timeout=10000)
    human_delay(0.2, 0.5)
    mobile_input.fill("")
    human_type(mobile_input, mobile, 60, 150)
    human_delay(0.3, 0.7)

    pw_input = page.locator("input[type='password']").first
    pw_input.click(timeout=10000)
    human_delay(0.15, 0.4)
    pw_input.fill("")
    human_type(pw_input, password, 50, 130)
    human_delay(0.4, 0.9)

    pw_input.press("Enter")
    time.sleep(random.uniform(2.5, 4))

    if "/login" in page.url.lower():
        for sel in ["input[type='submit'][value='Log In']", "input[value='Log In']",
                    "button:has-text('Log In')", "button:has-text('Login')", "[type='submit']"]:
            try:
                btn = page.locator(sel).first
                if btn.count() > 0 and btn.is_visible(timeout=2000):
                    btn.click(timeout=5000)
                    break
            except Exception:
                continue
        time.sleep(random.uniform(2.5, 4))

    try:
        page.wait_for_selector("text=DEPOSIT", timeout=20000)
    except PlaywrightTimeout:
        try:
            page.wait_for_selector("text=Deposit", timeout=5000)
        except PlaywrightTimeout:
            page.wait_for_url(lambda u: "/login" not in u.lower(), timeout=15000)
    log(f"Login successful!")


# ═══════════════════════════════════════════════════════════
#  NAVIGATE TO AVIATOR
# ═══════════════════════════════════════════════════════════

def navigate_to_aviator(page):
    log("Navigating to Aviator game...")
    IFRAME_SELS = [
        "iframe[src*='spribegaming.com/aviator']",
        "iframe[src*='spribegaming.com']",
        "iframe[src*='aviator']",
        "iframe.game-iframe",
        "iframe",
    ]
    for attempt in range(3):
        if attempt > 0:
            page.reload(wait_until="domcontentloaded", timeout=30000)
        else:
            page.goto(AVIATOR_URL, wait_until="domcontentloaded", timeout=30000)
        try:
            page.wait_for_load_state("load", timeout=20000)
        except Exception:
            pass
        human_delay(4, 6)

        for sel in IFRAME_SELS:
            try:
                iloc = page.locator(sel).first
                if iloc.count() > 0:
                    iloc.wait_for(state="visible", timeout=20000)
                    frame = page.frame_locator(sel).first
                    try:
                        frame.locator("button.btn-success.bet").wait_for(
                            state="visible", timeout=15000)
                        log("Aviator game loaded!")
                        return frame
                    except Exception:
                        pass
            except Exception:
                continue
    raise RuntimeError("Could not load Aviator game.")


# ═══════════════════════════════════════════════════════════
#  BALANCE
# ═══════════════════════════════════════════════════════════

def get_balance(frame):
    try:
        text = frame.locator("span.balance-amount").first.text_content(timeout=3000)
        if text:
            cleaned = text.strip().replace(",", "").replace(" ", "")
            return float(re.sub(r"[^\d.]", "", cleaned) or "0")
    except Exception:
        pass
    return None


# ═══════════════════════════════════════════════════════════
#  PLACE BET
# ═══════════════════════════════════════════════════════════

def place_bet(frame, amount):
    try:
        bet_input = frame.locator("input[inputmode='decimal'][type='text']").first
        bet_input.wait_for(state="visible", timeout=8000)

        deadline = time.time() + 10
        while time.time() < deadline:
            try:
                if bet_input.is_enabled():
                    break
            except Exception:
                pass
            time.sleep(0.1)

        bet_input.click(timeout=5000)
        human_delay(0.05, 0.15)
        bet_input.fill("")
        if amount == int(amount):
            bet_input.fill(str(int(amount)))
        else:
            bet_input.fill(f"{amount:.1f}")
        human_delay(0.1, 0.2)

        bet_btn = frame.locator("button.btn-success.bet").first
        bet_btn.click(timeout=6000)

        state.bet_placed = True
        return True
    except Exception as e:
        log(f"  Bet placement failed: {e}", "ERROR")
        return False


# ═══════════════════════════════════════════════════════════
#  WAIT FOR BET BUTTON
# ═══════════════════════════════════════════════════════════

def wait_for_bet_button(frame, timeout=120):
    start = time.time()
    while time.time() - start < timeout:
        try:
            btn = frame.locator("button.btn-success.bet").first
            if btn.count() > 0 and btn.is_visible(timeout=500):
                text = (btn.text_content(timeout=500) or "").strip().lower()
                if "bet" in text:
                    try:
                        if not btn.is_disabled(timeout=300):
                            return True
                    except Exception:
                        pass
        except Exception:
            pass
        time.sleep(0.3)
    return False


# ═══════════════════════════════════════════════════════════
#  CASHOUT — DETAILED LOGGING VERSION
# ═══════════════════════════════════════════════════════════

CASHOUT_SELECTORS = [
    "button.btn-warning.cashout",
    "button.cashout",
    "button.btn-warning",
    "[class*='cashout'] button",
    "button[class*='cashout']",
    "button[class*='cash-out']",
    "button:has-text('CASH OUT')",
    "button:has-text('Cashout')",
    "button:has-text('Cash Out')",
]


def click_cashout_button(frame):
    """Try to click the cashout button. Returns (clicked: bool, method: str)."""

    # === Attempt 1: Playwright selectors (no scroll — force click) ===
    for sel in CASHOUT_SELECTORS:
        try:
            btn = frame.locator(sel).first
            count = btn.count()
            if count > 0:
                visible = btn.is_visible(timeout=300)
                if visible:
                    btn.click(timeout=800, force=True)
                    return True, f"PW:{sel}"
                else:
                    log(f"    [CASHOUT-DBG] {sel}: found ({count}) but NOT visible")
        except Exception as e:
            log(f"    [CASHOUT-DBG] {sel}: exception: {str(e)[:60]}")
            continue

    # === Attempt 2: Dump all buttons in the iframe for debugging ===
    try:
        all_buttons = frame.locator("html").evaluate("""() => {
            const btns = document.querySelectorAll('button');
            return Array.from(btns).map(b => ({
                tag: b.tagName,
                classes: b.className,
                text: (b.textContent || '').trim().substring(0, 50),
                visible: b.offsetParent !== null,
                disabled: b.disabled,
                rect: b.getBoundingClientRect()
            }));
        }""")
        if all_buttons:
            log(f"    [CASHOUT-DBG] All buttons in iframe ({len(all_buttons)}):")
            for i, b in enumerate(all_buttons[:15]):
                log(f"      [{i}] class='{b['classes']}' text='{b['text']}' "
                    f"visible={b['visible']} disabled={b['disabled']} "
                    f"rect={b['rect']['width']:.0f}x{b['rect']['height']:.0f}")
    except Exception as e:
        log(f"    [CASHOUT-DBG] Failed to dump buttons: {str(e)[:60]}")

    # === Attempt 3: JavaScript click fallback ===
    try:
        js_clicked = frame.locator("html").evaluate("""() => {
            const selectors = [
                'button.btn-warning.cashout',
                'button.cashout',
                'button.btn-warning',
                'button[class*="cashout"]',
                'button[class*="cash-out"]'
            ];
            for (const sel of selectors) {
                const btn = document.querySelector(sel);
                if (btn && btn.offsetParent !== null) {
                    btn.click();
                    return 'js:' + sel;
                }
            }
            const allBtns = document.querySelectorAll('button');
            for (const btn of allBtns) {
                const txt = (btn.textContent || '').toLowerCase();
                if ((txt.includes('cash') || txt.includes('cashout')) && btn.offsetParent !== null) {
                    btn.click();
                    return 'js:text:' + txt.trim().substring(0, 30);
                }
            }
            return null;
        }""")
        if js_clicked:
            return True, f"JS:{js_clicked}"
    except Exception as e:
        log(f"    [CASHOUT-DBG] JS fallback error: {str(e)[:60]}")

    return False, "NONE"


def do_cashout(page, frame, multiplier_at_cashout):
    """
    Try cashout: button click (2 attempts) + page refresh backup.
    Returns (new_frame, cashout_info).
    """
    cashout_info = {"button_clicked": False, "refresh_done": False, "method": "none"}

    # First attempt
    log(f"  >>> Attempting cashout at {multiplier_at_cashout:.2f}x...")
    clicked, method = click_cashout_button(frame)
    if clicked:
        log(f"  >>> CASHOUT BUTTON CLICKED! method={method}")
        cashout_info["button_clicked"] = True
        cashout_info["method"] = method
        # Give server time to process
        try:
            page.wait_for_timeout(500)
        except Exception:
            pass
        # Don't refresh if button worked — check balance instead
        return frame, cashout_info

    # Retry after short delay
    log(f"  >>> First attempt failed, retrying in 200ms...")
    try:
        page.wait_for_timeout(200)
    except Exception:
        pass
    clicked, method = click_cashout_button(frame)
    if clicked:
        log(f"  >>> CASHOUT BUTTON CLICKED on retry! method={method}")
        cashout_info["button_clicked"] = True
        cashout_info["method"] = method
        try:
            page.wait_for_timeout(500)
        except Exception:
            pass
        return frame, cashout_info

    # Fallback: page refresh
    log(f"  >>> Button failed both attempts — REFRESHING PAGE as backup")
    page.reload(wait_until="domcontentloaded", timeout=30000)
    cashout_info["refresh_done"] = True
    cashout_info["method"] = "refresh"
    try:
        page.wait_for_load_state("load", timeout=15000)
    except Exception:
        pass
    human_delay(2, 4)

    page.locator("iframe[src*='spribegaming.com/aviator']").first.wait_for(
        state="visible", timeout=25000)
    new_frame = page.frame_locator("iframe[src*='spribegaming.com/aviator']").first
    new_frame.locator("button.btn-success.bet").wait_for(state="visible", timeout=20000)
    return new_frame, cashout_info


# ═══════════════════════════════════════════════════════════
#  MONITOR ROUND
# ═══════════════════════════════════════════════════════════

def monitor_round(page, frame, cashout_target):
    log("  Waiting for plane to take off...")
    t0 = time.time()
    started = False
    last_debug = 0

    while time.time() - t0 < 60:
        if state.round_active:
            mult = state.get_multiplier()
            if mult >= 1.0:
                started = True
                log(f"  Plane flying! {mult:.2f}x  (target: {cashout_target:.1f}x)")
                break
        if state.crashed:
            log("  Plane crashed before takeoff!", "WARN")
            return ("loss", state.get_multiplier(), frame, None)

        # Debug log every 5 seconds while waiting
        elapsed = time.time() - t0
        if elapsed - last_debug >= 5:
            last_debug = elapsed
            log(f"  [DBG] Still waiting... round_active={state.round_active} "
                f"mult={state.get_multiplier():.2f} crashed={state.crashed} "
                f"ws_connected={state.ws_connected} msgs={state.ws_msg_count} "
                f"betting_open={state.betting_open}")

        page.wait_for_timeout(50)

    if not started:
        log(f"  [DBG] Takeoff timeout! round_active={state.round_active} "
            f"mult={state.get_multiplier():.2f} crashed={state.crashed} "
            f"ws_connected={state.ws_connected} msgs={state.ws_msg_count}", "WARN")
        return ("error", 0.0, frame, None)

    log(f"  Monitoring... cashout at >= {cashout_target:.1f}x")

    while True:
        current = state.get_multiplier()

        if current >= cashout_target:
            log(f"  === TARGET HIT: {current:.2f}x >= {cashout_target:.1f}x ===")
            new_frame, cashout_info = do_cashout(page, frame, current)
            return ("win", current, new_frame, cashout_info)

        if state.crashed:
            crash_val = state.get_multiplier()
            return ("loss", crash_val, frame, None)

        if time.time() - t0 > 300:
            return ("error", 0.0, frame, None)

        page.wait_for_timeout(30)


# ═══════════════════════════════════════════════════════════
#  MAIN TEST LOOP
# ═══════════════════════════════════════════════════════════

def run():
    log("=" * 60)
    log("  CASHOUT TEST BOT")
    log(f"  Stake: {STAKE} UGX  |  Target: >= {CASHOUT_TARGET}x")
    log(f"  Max rounds: {MAX_ROUNDS}")
    log("=" * 60)

    stats = {
        "wins": 0, "losses": 0, "phantoms": 0,
        "btn_cashouts": 0, "ref_cashouts": 0,
        "profit": 0.0
    }

    with sync_playwright() as p:
        browser = p.chromium.launch(
            headless=False,
            args=["--no-sandbox", "--disable-setuid-sandbox",
                  "--disable-dev-shm-usage"]
        )
        context = browser.new_context(
            viewport={"width": 1280, "height": 800},
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                       "AppleWebKit/537.36 (KHTML, like Gecko) "
                       "Chrome/120.0.0.0 Safari/537.36"
        )
        page = context.new_page()
        setup_websocket_interception(page)

        login(page)
        human_delay(1, 2)
        frame = navigate_to_aviator(page)

        # Wait for WS
        log("Waiting for WebSocket connection...")
        t0 = time.time()
        while time.time() - t0 < 30:
            if state.ws_connected and state.ws_msg_count > 0:
                break
            page.wait_for_timeout(500)
        if not state.ws_connected:
            log("WebSocket failed to connect!", "ERROR")
            browser.close()
            return

        log(f"WebSocket connected! ({state.ws_msg_count} messages received)")

        start_balance = get_balance(frame) or 0
        log(f"Starting balance: {start_balance:.0f} UGX")

        for round_num in range(1, MAX_ROUNDS + 1):
            print()
            log("─" * 55)
            log(f"ROUND #{round_num}  |  W:{stats['wins']} L:{stats['losses']} "
                f"P:{stats['phantoms']}  |  BTN:{stats['btn_cashouts']} "
                f"REF:{stats['ref_cashouts']}  |  P/L:{stats['profit']:+.1f}")
            log("─" * 55)

            # ── Wait for betting phase (same logic as cjavi.py) ──
            state.reset_round()
            log(f"  Waiting for betting phase...")

            bet_ready = False
            t0 = time.time()
            while time.time() - t0 < 120:
                if state.betting_open:
                    if wait_for_bet_button(frame, timeout=5):
                        bet_ready = True
                        break
                if wait_for_bet_button(frame, timeout=2):
                    if not state.round_active or state.get_multiplier() < 1.0:
                        bet_ready = True
                        break
                    else:
                        page.wait_for_timeout(1000)
                        continue
                page.wait_for_timeout(300)

            if not bet_ready:
                log("  Bet button not found — refreshing", "WARN")
                page.reload(wait_until="domcontentloaded", timeout=30000)
                human_delay(3, 5)
                try:
                    page.locator("iframe[src*='spribegaming.com/aviator']").first.wait_for(
                        state="visible", timeout=20000)
                    frame = page.frame_locator("iframe[src*='spribegaming.com/aviator']").first
                    frame.locator("button.btn-success.bet").wait_for(
                        state="visible", timeout=15000)
                except Exception:
                    log("  Reload failed", "ERROR")
                    page.wait_for_timeout(5000)
                continue

            # Capture pre-bet balance
            balance_before = get_balance(frame) or 0
            log(f"  Pre-bet balance: {balance_before:.0f} UGX")

            # Place bet
            log(f"  Placing bet: {STAKE} UGX (target: {CASHOUT_TARGET}x)")
            human_delay(0.2, 0.5)
            if not place_bet(frame, STAKE):
                log("  Bet failed!", "ERROR")
                page.wait_for_timeout(5000)
                continue

            log(f"  BET PLACED: {STAKE} UGX")

            # Reset for monitoring
            state.crashed = False
            state.round_active = False
            state.set_multiplier(0.0)

            # Monitor round
            result, mult, frame, cashout_info = monitor_round(page, frame, CASHOUT_TARGET)

            if result == "win":
                # Check balance to verify
                human_delay(1, 2)
                balance_after = get_balance(frame) or 0
                log(f"  Balance check: {balance_before:.0f} → {balance_after:.0f} UGX")

                if balance_after > balance_before:
                    profit = balance_after - balance_before
                    stats["wins"] += 1
                    stats["profit"] += profit
                    if cashout_info and cashout_info.get("button_clicked"):
                        stats["btn_cashouts"] += 1
                        log(f"  +++ WIN (BUTTON) +++ at {mult:.2f}x  "
                            f"profit: +{profit:.1f} UGX  method: {cashout_info.get('method')}")
                    elif cashout_info and cashout_info.get("refresh_done"):
                        stats["ref_cashouts"] += 1
                        log(f"  +++ WIN (REFRESH) +++ at {mult:.2f}x  "
                            f"profit: +{profit:.1f} UGX")
                    else:
                        log(f"  +++ WIN +++ at {mult:.2f}x  profit: +{profit:.1f} UGX")
                else:
                    stats["phantoms"] += 1
                    stats["profit"] -= STAKE
                    log(f"  !!! PHANTOM CASHOUT !!!  at {mult:.2f}x", "WARN")
                    log(f"  Balance did not increase: {balance_before:.0f} → {balance_after:.0f}")
                    if cashout_info:
                        log(f"  Method attempted: {cashout_info.get('method', 'unknown')}")

            elif result == "loss":
                stats["losses"] += 1
                stats["profit"] -= STAKE
                log(f"  --- LOSS --- Crashed at {mult:.2f}x (needed {CASHOUT_TARGET}x)")

            elif result == "error":
                stats["losses"] += 1
                stats["profit"] -= STAKE
                log(f"  --- ERROR --- (counted as loss)")

            human_delay(0.5, 1.5)

        # Final summary
        print()
        log("=" * 60)
        log("  TEST COMPLETE")
        log("=" * 60)
        end_balance = get_balance(frame) or 0
        log(f"  Rounds played    : {stats['wins'] + stats['losses'] + stats['phantoms']}")
        log(f"  Wins             : {stats['wins']}")
        log(f"  Losses           : {stats['losses']}")
        log(f"  Phantom cashouts : {stats['phantoms']}")
        log(f"  Button cashouts  : {stats['btn_cashouts']}")
        log(f"  Refresh cashouts : {stats['ref_cashouts']}")
        log(f"  Profit           : {stats['profit']:+.1f} UGX")
        log(f"  Balance          : {start_balance:.0f} → {end_balance:.0f} UGX")
        log("=" * 60)

        browser.close()


if __name__ == "__main__":
    try:
        run()
    except KeyboardInterrupt:
        log("Stopped by user (Ctrl+C)")
    except Exception as e:
        log(f"Fatal error: {e}", "ERROR")
        import traceback
        traceback.print_exc()
