#!/usr/bin/env python3
"""
Phase 3.5: Automated Runtime UI Capture via ADB + UIAutomator

Automatically builds, installs, and captures runtime UI data from each screen.
Supports Compose apps by navigating via bottom tabs and capturing UIAutomator dumps
plus screenshots.

Zero code invasion — pure ADB-based approach.

Usage:
    python3 runtime_dump.py <project_root> <output_dir>

Prerequisites:
    - ADB accessible in PATH
    - Connected device/emulator (adb devices shows at least one)
    - Gradle wrapper in project root (for building debug APK)

Output:
    <output_dir>/runtime_dumps/<ScreenName>.xml      — UIAutomator dump
    <output_dir>/runtime_dumps/<ScreenName>.png       — Screenshot
    <output_dir>/runtime_dumps/<ScreenName>_scrolled.xml  — After-scroll dump (if scrollable)
    <output_dir>/runtime_dumps/runtime_summary.json   — Summary with status per screen
"""

import json
import os
import re
import subprocess
import sys
import time
import xml.etree.ElementTree as ET


# ---------------------------------------------------------------------------
# ADB helpers
# ---------------------------------------------------------------------------

def run_adb(args, timeout=15):
    """Run an adb command and return (returncode, stdout, stderr)."""
    cmd = ["adb"] + args
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
        return result.returncode, result.stdout.strip(), result.stderr.strip()
    except subprocess.TimeoutExpired:
        return -1, "", "timeout"
    except FileNotFoundError:
        return -1, "", "adb not found"


def check_device() -> tuple[bool, str]:
    """Check that adb is available and a device/emulator is connected."""
    rc, out, err = run_adb(["version"])
    if rc != 0:
        return False, "adb not found or not working"

    rc, out, err = run_adb(["devices"])
    lines = [l for l in out.split("\n") if l.strip() and not l.startswith("List")]
    devices = [l for l in lines if "device" in l and "offline" not in l and "unauthorized" not in l]
    if not devices:
        return False, "No connected device/emulator found"

    return True, f"Device ready ({devices[0].split()[0]})"


def check_app_installed(package: str) -> bool:
    """Check if an app is installed on the device."""
    rc, out, err = run_adb(["shell", "pm", "list", "packages", package])
    return package in out


def is_app_debuggable(package: str) -> bool:
    """Check if the installed app is a debug build (debuggable=true).
    Non-exported Activities can only be launched via adb on debug builds."""
    rc, out, err = run_adb(["shell", "run-as", package, "true"])
    return rc == 0


def find_sample_media_on_device() -> dict:
    """Find sample image and video files on the device for launching intent-data Activities.
    Returns dict with 'image' and 'video' keys pointing to file URIs."""
    media = {"image": None, "video": None}

    # Find a sample image
    rc, out, _ = run_adb(["shell", "find", "/sdcard/DCIM", "/sdcard/Pictures", "/sdcard/Download",
                          "-maxdepth", "3", "-type", "f",
                          "(", "-name", "*.jpg", "-o", "-name", "*.png", ")"],
                         timeout=10)
    if rc == 0 and out.strip():
        first_file = out.strip().split("\n")[0]
        media["image"] = f"file://{first_file}"

    # Find a sample video
    rc, out, _ = run_adb(["shell", "find", "/sdcard/DCIM", "/sdcard/Movies", "/sdcard/Download",
                          "-maxdepth", "3", "-type", "f",
                          "(", "-name", "*.mp4", "-o", "-name", "*.3gp", ")"],
                         timeout=10)
    if rc == 0 and out.strip():
        first_file = out.strip().split("\n")[0]
        media["video"] = f"file://{first_file}"

    return media


def launch_activity_with_data(package: str, activity_class: str,
                              data_uri: str, mime_type: str) -> tuple[bool, str]:
    """Launch an activity with intent data URI and MIME type."""
    if activity_class.startswith(package + "."):
        suffix = activity_class[len(package):]
        component = f"{package}/{suffix}"
    elif "." in activity_class and not activity_class.startswith("."):
        component = f"{package}/{activity_class}"
    elif activity_class.startswith("."):
        component = f"{package}/{activity_class}"
    else:
        component = f"{package}/.{activity_class}"

    rc, out, err = run_adb([
        "shell", "am", "start", "--user", "0",
        "-n", component,
        "-d", data_uri,
        "-t", mime_type,
    ], timeout=10)

    if rc != 0 or "Error" in err or "Error" in out:
        return False, f"Failed to launch with data: {(err or out).split(chr(10))[0]}"

    time.sleep(3)
    return True, "Launched with data"


def install_apk(apk_path: str) -> tuple[bool, str]:
    """Install an APK on the connected device."""
    rc, out, err = run_adb(["install", "-r", "-t", apk_path], timeout=120)
    if rc == 0 and "Success" in out:
        return True, "Installed"
    return False, f"Install failed: {err or out}"


def launch_activity(package: str, activity_class: str) -> tuple[bool, str]:
    """Launch an activity via ADB."""
    if activity_class.startswith(package + "."):
        suffix = activity_class[len(package):]
        component = f"{package}/{suffix}"
    elif "." in activity_class and not activity_class.startswith("."):
        component = f"{package}/{activity_class}"
    elif activity_class.startswith("."):
        component = f"{package}/{activity_class}"
    else:
        component = f"{package}/.{activity_class}"

    rc, out, err = run_adb(["shell", "am", "start", "--user", "0", "-n", component], timeout=10)
    if rc != 0 or "Error" in err or "Error" in out:
        return False, f"Failed to launch: {(err or out).split(chr(10))[0]}"

    time.sleep(3)
    return True, "Launched"


def dump_ui(output_path: str) -> tuple[bool, str]:
    """Capture uiautomator dump and pull to local path."""
    remote_path = "/sdcard/ui_dump.xml"
    run_adb(["shell", "rm", "-f", remote_path])

    rc, out, err = run_adb(["shell", "uiautomator", "dump", remote_path], timeout=15)
    if rc != 0 or "error" in (out + err).lower():
        return False, f"uiautomator dump failed: {err or out}"

    rc, out, err = run_adb(["pull", remote_path, output_path], timeout=10)
    if rc != 0:
        return False, f"adb pull failed: {err}"

    if not os.path.isfile(output_path) or os.path.getsize(output_path) < 100:
        return False, "Dump file empty or missing"

    return True, "OK"


def take_screenshot(output_path: str) -> tuple[bool, str]:
    """Capture a screenshot and pull to local path."""
    remote_path = "/sdcard/screenshot.png"
    rc, out, err = run_adb(["shell", "screencap", "-p", remote_path], timeout=10)
    if rc != 0:
        return False, f"screencap failed: {err}"

    rc, out, err = run_adb(["pull", remote_path, output_path], timeout=10)
    if rc != 0:
        return False, f"pull failed: {err}"

    run_adb(["shell", "rm", "-f", remote_path])
    return True, "OK"


def tap(x: int, y: int):
    """Tap a screen coordinate."""
    run_adb(["shell", "input", "tap", str(x), str(y)])
    time.sleep(0.3)


def swipe_up(cx: int, start_y: int, end_y: int, duration_ms: int = 500):
    """Swipe up (scroll down) on the screen."""
    run_adb(["shell", "input", "swipe", str(cx), str(start_y), str(cx), str(end_y), str(duration_ms)])
    time.sleep(1)


def press_back():
    """Press the back button."""
    run_adb(["shell", "input", "keyevent", "KEYCODE_BACK"])
    time.sleep(0.5)


def press_home():
    """Press the home button."""
    run_adb(["shell", "input", "keyevent", "KEYCODE_HOME"])
    time.sleep(1)


def get_screen_size() -> tuple[int, int]:
    """Get device screen width and height."""
    rc, out, err = run_adb(["shell", "wm", "size"])
    m = re.search(r'(\d+)x(\d+)', out)
    if m:
        return int(m.group(1)), int(m.group(2))
    return 1080, 2400  # fallback


# ---------------------------------------------------------------------------
# XML parsing helpers
# ---------------------------------------------------------------------------

def find_nav_items(dump_path: str) -> list[dict]:
    """Parse UIAutomator dump XML to find bottom navigation tab items.

    Handles two patterns:
      A) Clickable nodes with text/content-desc in bottom area
      B) Clickable container Views in bottom area with child TextViews (Compose pattern)

    Returns list of {text, label, center_x, center_y, bounds, content_desc}.
    """
    try:
        tree = ET.parse(dump_path)
    except ET.ParseError:
        return []

    items = []
    root = tree.getroot()

    all_nodes = list(root.iter("node"))
    screen_h = 0
    for node in all_nodes:
        bounds = node.get("bounds", "")
        m = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds)
        if m:
            screen_h = max(screen_h, int(m.group(4)))

    if screen_h == 0:
        return []

    bottom_threshold = screen_h * 0.85

    # Strategy A: clickable nodes with text in bottom area
    for node in all_nodes:
        text = node.get("text", "")
        desc = node.get("content-desc", "")
        clickable = node.get("clickable", "false") == "true"
        bounds = node.get("bounds", "")
        m = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds)
        if not m:
            continue
        left, top, right, bottom = int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))
        if top < bottom_threshold:
            continue
        label = text or desc
        if not label or not clickable:
            continue
        width, height = right - left, bottom - top
        if width > screen_h * 0.5 or height > screen_h * 0.15:
            continue
        items.append({
            "text": text, "content_desc": desc, "label": label,
            "center_x": (left + right) // 2, "center_y": (top + bottom) // 2,
            "bounds": {"left": left, "top": top, "right": right, "bottom": bottom},
        })

    # Strategy B: clickable container Views with child TextViews (Compose bottom nav)
    if not items:
        for node in all_nodes:
            clickable = node.get("clickable", "false") == "true"
            bounds = node.get("bounds", "")
            m = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds)
            if not m:
                continue
            left, top, right, bottom = int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))
            if top < bottom_threshold or not clickable:
                continue
            width, height = right - left, bottom - top
            if width > screen_h * 0.4 or width < 50 or height > screen_h * 0.15 or height < 50:
                continue
            # Search for child text node within this clickable container's bounds
            label = ""
            for child in all_nodes:
                child_text = child.get("text", "") or child.get("content-desc", "")
                if not child_text:
                    continue
                cb = child.get("bounds", "")
                cm = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', cb)
                if not cm:
                    continue
                cl, ct, cr, cbot = int(cm.group(1)), int(cm.group(2)), int(cm.group(3)), int(cm.group(4))
                if cl >= left and ct >= top and cr <= right and cbot <= bottom:
                    label = child_text
                    break
            if label:
                items.append({
                    "text": label, "content_desc": "", "label": label,
                    "center_x": (left + right) // 2, "center_y": (top + bottom) // 2,
                    "bounds": {"left": left, "top": top, "right": right, "bottom": bottom},
                })

    # Deduplicate by position (items too close together are the same)
    unique = []
    for item in items:
        is_dup = False
        for existing in unique:
            if abs(item["center_x"] - existing["center_x"]) < 30 and abs(item["center_y"] - existing["center_y"]) < 30:
                is_dup = True
                break
        if not is_dup:
            unique.append(item)

    # Sort left to right
    unique.sort(key=lambda x: x["center_x"])

    return unique


def count_nodes(dump_path: str) -> int:
    """Count nodes in a UIAutomator dump XML."""
    try:
        tree = ET.parse(dump_path)
        return sum(1 for _ in tree.getroot().iter())
    except Exception:
        return 0


# ---------------------------------------------------------------------------
# Package detection
# ---------------------------------------------------------------------------

def detect_package(project_root: str, discovery_path: str | None = None) -> str | None:
    """Detect the app package name from discovery.json, build.gradle, or AndroidManifest.xml."""
    # Try discovery.json first — it has a direct "package" field
    if discovery_path and os.path.isfile(discovery_path):
        with open(discovery_path, "r") as f:
            discovery = json.load(f)
        pkg = discovery.get("package")
        if pkg:
            return pkg

    # Try build.gradle.kts / build.gradle for applicationId or namespace
    for root, _, files in os.walk(project_root):
        if "/build/" in root or "\\build\\" in root:
            continue
        for f in files:
            if f in ("build.gradle.kts", "build.gradle") and "app" in root:
                fpath = os.path.join(root, f)
                try:
                    with open(fpath, "r") as fh:
                        content = fh.read()
                    # applicationId = "com.yunhou.gaokao"
                    m = re.search(r'applicationId\s*=?\s*"([^"]+)"', content)
                    if m:
                        return m.group(1)
                    # namespace = "com.yunhou.gaokao"
                    m = re.search(r'namespace\s*=?\s*"([^"]+)"', content)
                    if m:
                        return m.group(1)
                except Exception:
                    pass

    # Try discovery.json
    if discovery_path and os.path.isfile(discovery_path):
        with open(discovery_path, "r") as f:
            discovery = json.load(f)
        activities = discovery.get("activities", [])
        for act in activities:
            cls = act.get("class", "")
            # Full class name: com.yunhou.gaokao.MainActivity → com.yunhou.gaokao
            if "." in cls and not cls.startswith("."):
                parts = cls.rsplit(".", 1)
                if len(parts) == 2 and parts[0].count(".") >= 1:
                    return parts[0]

    # Try AndroidManifest.xml
    manifest_path = None
    for root, _, files in os.walk(project_root):
        if "AndroidManifest.xml" in files:
            candidate = os.path.join(root, "AndroidManifest.xml")
            if "app/src/main" in candidate:
                manifest_path = candidate
                break
            if manifest_path is None:
                manifest_path = candidate

    if manifest_path:
        tree = ET.parse(manifest_path)
        root = tree.getroot()
        package = root.get("package")
        if package:
            return package

    return None


# ---------------------------------------------------------------------------
# APK build & install
# ---------------------------------------------------------------------------

def find_debug_apk(project_root: str) -> str | None:
    """Find a debug APK in the project's build output."""
    apk_dir = os.path.join(project_root, "app", "build", "outputs", "apk")
    if not os.path.isdir(apk_dir):
        return None

    # Search for any debug APK
    for root, _, files in os.walk(apk_dir):
        for f in files:
            if f.endswith(".apk") and "debug" in f.lower():
                return os.path.join(root, f)

    return None


def build_debug_apk(project_root: str) -> tuple[bool, str]:
    """Build a debug APK using Gradle.

    Auto-detects product flavors and JAVA_HOME for JDK 17.
    """
    gradlew = os.path.join(project_root, "gradlew")
    if not os.path.isfile(gradlew):
        return False, "gradlew not found"

    # Make gradlew executable
    os.chmod(gradlew, 0o755)

    # Auto-detect JAVA_HOME: prefer JDK 17 for AGP 8.x compatibility
    env = os.environ.copy()
    if sys.platform == "darwin":
        try:
            result = subprocess.run(
                ["/usr/libexec/java_home", "-v", "17"],
                capture_output=True, text=True, timeout=5)
            if result.returncode == 0 and result.stdout.strip():
                env["JAVA_HOME"] = result.stdout.strip()
                print(f"  Using JAVA_HOME: {env['JAVA_HOME']}")
        except Exception:
            pass

    # Detect product flavors from build.gradle to choose the right task
    build_task = "assembleDebug"
    for build_file in ["app/build.gradle.kts", "app/build.gradle"]:
        build_path = os.path.join(project_root, build_file)
        if os.path.isfile(build_path):
            with open(build_path, "r") as f:
                content = f.read()
            # Check for productFlavors with common FOSS/free flavors
            if "productFlavors" in content:
                for flavor in ["foss", "free", "fdroid", "oss"]:
                    if f'"{flavor}"' in content or f"'{flavor}'" in content:
                        build_task = f"assemble{flavor.capitalize()}Debug"
                        print(f"  Detected product flavor: {flavor} -> {build_task}")
                        break
                else:
                    # Use first registered flavor
                    m = re.search(r'register\("(\w+)"\)', content)
                    if m:
                        flavor = m.group(1)
                        build_task = f"assemble{flavor.capitalize()}Debug"
                        print(f"  Using first flavor: {flavor} -> {build_task}")
            break

    print(f"  Building debug APK ({build_task}, this may take a few minutes)...")
    try:
        result = subprocess.run(
            [gradlew, build_task, "-x", "lint", "-x", "test"],
            cwd=project_root,
            capture_output=True,
            text=True,
            timeout=600,
            env=env,
        )
        if result.returncode != 0:
            # Extract useful error message
            error_lines = [l for l in result.stderr.split("\n") if "ERROR" in l or "FAILURE" in l]
            return False, f"Build failed: {' '.join(error_lines[:3])}" if error_lines else "Build failed"
        return True, "Build successful"
    except subprocess.TimeoutExpired:
        return False, "Build timed out (10 minutes)"


# ---------------------------------------------------------------------------
# Main capture logic
# ---------------------------------------------------------------------------

def capture_activity(package: str, activity: dict, dumps_dir: str,
                     sample_media: dict | None = None) -> dict:
    """Capture runtime UI for a single Activity.

    Args:
        sample_media: dict with 'image' and 'video' file URIs for intent-data Activities.
    """
    simple_name = activity["simple_name"]
    activity_class = activity["class"]

    # Skip splash screens
    if "splash" in simple_name.lower():
        return {"screen": simple_name, "status": "skipped", "reason": "splash screen"}

    # Skip SDK/callback activities
    skip_keywords = ["WXEntryActivity", "WXPayEntryActivity", "GenLoginAuthActivity"]
    if simple_name in skip_keywords:
        return {"screen": simple_name, "status": "skipped", "reason": "SDK callback activity"}

    # Determine if this Activity needs intent data based on common patterns
    intent_data_hints = {
        "image": ["Photo", "Edit", "Wallpaper", "Panorama"],
        "video": ["Video"],
    }
    needs_data_type = None
    for media_type, keywords in intent_data_hints.items():
        if any(kw.lower() in simple_name.lower() for kw in keywords):
            needs_data_type = media_type
            break

    press_home()

    # Try 1: Direct launch (works for most Activities on debug builds)
    ok, msg = launch_activity(package, activity_class)

    # Try 2: If direct launch fails and Activity likely needs intent data, try with sample media
    if not ok and needs_data_type and sample_media:
        data_uri = sample_media.get(needs_data_type)
        mime_type = f"{needs_data_type}/*"
        if data_uri:
            ok, msg = launch_activity_with_data(package, activity_class, data_uri, mime_type)

    # Try 3: Retry once with longer wait on transient failures
    if not ok and "SecurityException" not in msg:
        time.sleep(2)
        press_home()
        ok, msg = launch_activity(package, activity_class)

    if not ok:
        return {"screen": simple_name, "status": "failed", "reason": msg}

    # Dump
    dump_path = os.path.join(dumps_dir, f"{simple_name}.xml")
    ok, msg = dump_ui(dump_path)
    if not ok:
        return {"screen": simple_name, "status": "failed", "reason": msg}

    # Screenshot
    screenshot_path = os.path.join(dumps_dir, f"{simple_name}.png")
    take_screenshot(screenshot_path)

    nodes = count_nodes(dump_path)

    return {
        "screen": simple_name,
        "status": "success",
        "nodes": nodes,
        "dump_file": os.path.basename(dump_path),
        "screenshot_file": os.path.basename(screenshot_path),
    }


def dismiss_dialogs(max_attempts: int = 3):
    """Auto-dismiss common startup dialogs (privacy policy, permissions, updates, etc.).

    Looks for common button texts and taps them.
    """
    dismiss_texts = {
        "同意并继续", "同意", "确定", "我知道了", "允许", "始终允许", "下一步", "跳过",
        "AGREE", "ACCEPT", "OK", "ALLOW", "GOT IT", "CONTINUE",
        "I AGREE", "NEXT", "SKIP", "同意并进入",
    }

    for attempt in range(max_attempts):
        time.sleep(2)
        tmp_dump = f"/tmp/_dialog_check_{attempt}.xml"
        ok, _ = dump_ui(tmp_dump)
        if not ok:
            break

        try:
            tree = ET.parse(tmp_dump)
        except ET.ParseError:
            break

        found_dialog_button = False
        for node in tree.getroot().iter("node"):
            text = (node.get("text", "") or "").strip()
            bounds = node.get("bounds", "")

            if not text:
                continue

            # Exact match only — avoid matching body text that contains "同意" etc.
            if text in dismiss_texts:
                m = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds)
                if m:
                    cx = (int(m.group(1)) + int(m.group(3))) // 2
                    cy = (int(m.group(2)) + int(m.group(4))) // 2
                    # The text might not be clickable itself, but its parent might be
                    # Tap the center of the text area regardless
                    print(f"    Dismissing dialog: '{text}' at ({cx}, {cy})")
                    tap(cx, cy)
                    time.sleep(2)
                    found_dialog_button = True
                    break

        try:
            os.remove(tmp_dump)
        except OSError:
            pass

        if not found_dialog_button:
            break  # No more dialogs


def capture_tab_screens(package: str, main_activity: dict, tab_names: list[str],
                        dumps_dir: str) -> list[dict]:
    """Navigate through bottom nav tabs and capture each screen.

    Returns list of capture results.
    """
    results = []
    activity_class = main_activity["class"]

    # Launch main activity
    press_home()
    ok, msg = launch_activity(package, activity_class)
    if not ok:
        return [{"screen": "MainActivity_tabs", "status": "failed", "reason": msg}]

    # Auto-dismiss startup dialogs (privacy policy, permissions, etc.)
    dismiss_dialogs()

    # Initial dump to find bottom nav items
    initial_dump = os.path.join(dumps_dir, "_initial_nav_detect.xml")
    ok, msg = dump_ui(initial_dump)
    if not ok:
        return [{"screen": "MainActivity_tabs", "status": "failed", "reason": f"Initial dump: {msg}"}]

    nav_items = find_nav_items(initial_dump)

    if not nav_items:
        print("    No bottom nav items detected — capturing current screen only")
        # Just capture default screen
        dump_path = os.path.join(dumps_dir, "Tab_Home.xml")
        os.rename(initial_dump, dump_path)
        screenshot_path = os.path.join(dumps_dir, "Tab_Home.png")
        take_screenshot(screenshot_path)
        return [{
            "screen": "Tab_Home",
            "status": "success",
            "nodes": count_nodes(dump_path),
            "dump_file": "Tab_Home.xml",
            "screenshot_file": "Tab_Home.png",
        }]

    # Clean up initial dump
    os.remove(initial_dump)

    print(f"    Found {len(nav_items)} bottom nav items: {[i['label'] for i in nav_items]}")

    # Get screen dimensions for scroll
    screen_w, screen_h = get_screen_size()

    # Capture each tab
    for idx, item in enumerate(nav_items):
        label = item["label"]
        safe_name = re.sub(r'[^\w]', '_', label)
        screen_name = f"Tab_{safe_name}"

        print(f"    Tab {idx + 1}/{len(nav_items)}: {label}...", end=" ", flush=True)

        # Tap the tab
        tap(item["center_x"], item["center_y"])
        time.sleep(2)  # Wait for screen transition

        # Dump
        dump_path = os.path.join(dumps_dir, f"{screen_name}.xml")
        ok, msg = dump_ui(dump_path)
        if not ok:
            print(f"FAIL ({msg})")
            results.append({"screen": screen_name, "status": "failed", "reason": msg})
            continue

        # Screenshot
        screenshot_path = os.path.join(dumps_dir, f"{screen_name}.png")
        take_screenshot(screenshot_path)

        nodes = count_nodes(dump_path)
        print(f"OK ({nodes} nodes)")

        result = {
            "screen": screen_name,
            "tab_label": label,
            "tab_index": idx,
            "status": "success",
            "nodes": nodes,
            "dump_file": f"{screen_name}.xml",
            "screenshot_file": f"{screen_name}.png",
        }

        # Scroll down and capture again (for below-fold content)
        cx = screen_w // 2
        swipe_up(cx, int(screen_h * 0.7), int(screen_h * 0.3))

        scrolled_dump = os.path.join(dumps_dir, f"{screen_name}_scrolled.xml")
        ok_s, _ = dump_ui(scrolled_dump)
        if ok_s:
            scrolled_nodes = count_nodes(scrolled_dump)
            if scrolled_nodes != nodes:
                result["scrolled_dump_file"] = f"{screen_name}_scrolled.xml"
                result["scrolled_nodes"] = scrolled_nodes
            else:
                os.remove(scrolled_dump)  # No change = not scrollable

        # Scroll back to top
        swipe_up(cx, int(screen_h * 0.3), int(screen_h * 0.7))
        time.sleep(0.5)

        results.append(result)

    return results


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main():
    if len(sys.argv) < 3:
        print("Usage: python3 runtime_dump.py <project_root> <output_dir>")
        sys.exit(1)

    project_root = os.path.abspath(sys.argv[1])
    output_dir = os.path.abspath(sys.argv[2])

    print("[Phase 3.5] Automated Runtime UI Capture")

    # Step 1: Check device
    ok, msg = check_device()
    print(f"  Device check: {msg}")
    if not ok:
        print("  No device available — skipping runtime capture (static analysis only)")
        sys.exit(0)

    # Step 1.5: Try adb root on emulators to bypass exported=false restriction
    rc, out, _ = run_adb(["root"], timeout=10)
    if rc == 0 and "root" in out.lower():
        print(f"  ADB root: enabled (can launch non-exported Activities on Android 12+)")
        time.sleep(2)  # Wait for adbd to restart
    else:
        print(f"  ADB root: not available (physical device or production emulator image)")

    # Step 2: Detect package
    discovery_path = os.path.join(output_dir, "discovery.json")
    package = detect_package(project_root, discovery_path)
    if not package:
        print("  ERROR: Could not detect app package name")
        sys.exit(1)
    print(f"  Package: {package}")

    # Step 3: Build and install APK if needed
    if not check_app_installed(package):
        print("  App not installed — building and installing debug APK...")
        apk_path = find_debug_apk(project_root)
        if not apk_path:
            ok, msg = build_debug_apk(project_root)
            if not ok:
                print(f"  Build failed: {msg}")
                print("  Skipping runtime capture")
                sys.exit(0)
            apk_path = find_debug_apk(project_root)

        if apk_path:
            print(f"  Installing: {os.path.basename(apk_path)}")
            ok, msg = install_apk(apk_path)
            if not ok:
                print(f"  Install failed: {msg}")
                sys.exit(0)
            print(f"  {msg}")
        else:
            print("  No debug APK found after build — skipping")
            sys.exit(0)
    else:
        print(f"  App already installed")

    # Step 3.5: Check if installed APK is debuggable
    debuggable = is_app_debuggable(package)
    if debuggable:
        print(f"  Debug build: YES — can launch non-exported Activities")
    else:
        print(f"  WARNING: Release build detected — non-exported Activities will fail!")
        print(f"  To fix: build and install a debug APK, then re-run this script.")
        print(f"    cd {project_root} && ./gradlew assembleDebug && adb install -r <debug-apk>")

    # Step 3.6: Find sample media files on device for intent-data Activities
    sample_media = find_sample_media_on_device()
    if sample_media.get("image"):
        print(f"  Sample image found: {sample_media['image']}")
    else:
        print(f"  No sample image found — intent-data Activities needing images will be skipped")
    if sample_media.get("video"):
        print(f"  Sample video found: {sample_media['video']}")
    else:
        print(f"  No sample video found — intent-data Activities needing videos will be skipped")

    # Step 4: Load discovery
    discovery = None
    if os.path.isfile(discovery_path):
        with open(discovery_path, "r") as f:
            discovery = json.load(f)

    # Create output dir
    dumps_dir = os.path.join(output_dir, "runtime_dumps")
    os.makedirs(dumps_dir, exist_ok=True)

    all_results = []

    # Step 5: Capture Activity screens
    activities = discovery.get("activities", []) if discovery else []
    main_activity = None

    if activities:
        print(f"\n  Capturing {len(activities)} Activities...")
        for activity in activities:
            if activity.get("is_launcher"):
                main_activity = activity

            result = capture_activity(package, activity, dumps_dir, sample_media)
            status = result["status"]
            icon = "+" if status == "success" else ("~" if status == "skipped" else "!")
            nodes_str = f" ({result.get('nodes', 0)} nodes)" if status == "success" else f" ({result.get('reason', '')})"
            print(f"    [{icon}] {result['screen']}{nodes_str}")
            all_results.append(result)

    # Step 6: Capture bottom nav tabs
    if main_activity:
        print(f"\n  Navigating bottom tabs...")
        tab_results = capture_tab_screens(package, main_activity, [], dumps_dir)
        all_results.extend(tab_results)

    # Step 7: Go home
    press_home()

    # Step 8: Write summary
    success = sum(1 for r in all_results if r["status"] == "success")
    skipped = sum(1 for r in all_results if r["status"] == "skipped")
    failed = sum(1 for r in all_results if r["status"] == "failed")

    summary = {
        "package": package,
        "total_screens": len(all_results),
        "success": success,
        "skipped": skipped,
        "failed": failed,
        "results": all_results,
    }

    summary_path = os.path.join(dumps_dir, "runtime_summary.json")
    with open(summary_path, "w", encoding="utf-8") as f:
        json.dump(summary, f, indent=2, ensure_ascii=False)

    print(f"\n[Phase 3.5 Complete]")
    print(f"  Success: {success}")
    print(f"  Skipped: {skipped}")
    print(f"  Failed: {failed}")
    print(f"  Output: {dumps_dir}/")


if __name__ == "__main__":
    main()