"""
getCoalescedEvents() CDP-injected-input discriminator — dual test runner.

Tests whether `pointermove.getCoalescedEvents().length` is smaller under
CDP-injected mouse events than under native (no-CDP) mouse events.

Per NIGHTLY-FINDINGS-2026-06-23 (Priority 1, UNVALIDATED claim from
WebDecoy/FCaptcha#10):

  Test A: vanilla playwright headless Chromium, no CDP.
  Test B: same, but with `page.context().new_cdp_session(page)` attached.
  Both perform identical 50-circle mouse moves. Measure per-event batch size.

Pass condition: Test B `max <= 1` (CDP events don't coalesce) while
Test A `max > 1` (real events coalesce). Spec claim: real mice coalesce
several hardware samples per frame; CDP `Input.dispatchMouseEvent` produces
single-entry batches.

Hits local page at http://127.0.0.1:3457/demo.html
"""
import asyncio
import json
import subprocess
import time
import os
import sys
import statistics

LOCAL_URL = "http://127.0.0.1:3457/demo.html"
CDP_PORT = 9336
CHROME_BIN = "/usr/bin/google-chrome"
USER_DATA_DIR = "/tmp/coalesced_cdp_test"

# 50 circle moves: radius 50, 50 steps, centered on target middle
CIRCLE_JS = """
async (page) => {
  const cx = 200, cy = 200, r = 50;
  for (let i = 0; i < 50; i++) {
    const angle = (i / 50) * Math.PI * 2;
    const x = cx + Math.cos(angle) * r;
    const y = cy + Math.sin(angle) * r;
    await page.mouse.move(x, y);
    await new Promise(r => setTimeout(r, 16)); // ~60Hz
  }
  await new Promise(r => setTimeout(r, 200));
}
"""


async def measure_coalesced(page):
    """Run the 50-circle mouse sequence and return coalesced batch stats."""
    # Reset sample buffer
    await page.evaluate("() => { window.__coalescedSamples = []; }")
    # Get target bounding box center
    box = await page.locator("#target").bounding_box()
    cx = box["x"] + box["width"] / 2
    cy = box["y"] + box["height"] / 2
    r = min(box["width"], box["height"]) / 4  # 50px radius for 200px target
    # 50 circle moves
    for i in range(50):
        angle = (i / 50) * 2 * 3.14159265
        x = cx + (r * 0.5) * (1 + 0.5 * (1 if i % 2 == 0 else -1))  # confined to target
        x = cx + (r * 0.5) * (1 + 0.5 * (1 if i % 2 == 0 else -1))
        y = cy + (r * 0.5) * (1 + 0.5 * (1 if i % 2 == 0 else -1))
        # Simpler: small jittered moves
        x = cx + r * 0.6 * (1 if i % 2 == 0 else -1)
        y = cy + r * 0.6 * (1 if i % 3 == 0 else -1)
        await page.mouse.move(x, y)
        await page.wait_for_timeout(16)  # ~60Hz
    await page.wait_for_timeout(200)
    samples = await page.evaluate("() => window.__coalescedSamples || []")
    if not samples:
        return None
    return {
        "count": len(samples),
        "max": max(samples),
        "avg": statistics.mean(samples),
        "median": statistics.median(samples),
        "min": min(samples),
        "ones": sum(1 for s in samples if s == 1),
        "twos_plus": sum(1 for s in samples if s >= 2),
    }


async def run_vanilla():
    """Test 1: vanilla Playwright Chromium, no CDP attachment."""
    print("=" * 70)
    print("TEST 1: Vanilla Playwright (no CDP)")
    print("=" * 70)
    from playwright.async_api import async_playwright
    async with async_playwright() as p:
        browser = await p.chromium.launch(
            headless=True,
            args=["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]
        )
        ctx = await browser.new_context()
        page = await ctx.new_page()
        await page.goto(LOCAL_URL, wait_until="networkidle", timeout=15000)
        await page.wait_for_timeout(1000)
        result = await measure_coalesced(page)
        print(json.dumps(result, indent=2) if result else "NO SAMPLES")
        await browser.close()
        return result


async def run_cdp():
    """Test 2: Chrome with --remote-debugging-port (CDP attached)."""
    print("=" * 70)
    print("TEST 2: Chrome with --remote-debugging-port (CDP ATTACHED)")
    print("=" * 70)
    if os.path.exists(USER_DATA_DIR):
        subprocess.run(["rm", "-rf", USER_DATA_DIR], check=False)
    proc = subprocess.Popen(
        [CHROME_BIN, "--headless=new", "--no-sandbox", "--disable-gpu",
         "--disable-dev-shm-usage", f"--remote-debugging-port={CDP_PORT}",
         f"--user-data-dir={USER_DATA_DIR}", "about:blank"],
        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
    )
    # Wait for CDP endpoint
    for _ in range(20):
        time.sleep(0.5)
        try:
            import urllib.request
            urllib.request.urlopen(f"http://127.0.0.1:{CDP_PORT}/json/version", timeout=1)
            break
        except Exception:
            continue
    else:
        print("CDP endpoint never came up")
        proc.terminate()
        return None

    from playwright.async_api import async_playwright
    result = None
    async with async_playwright() as p:
        browser = await p.chromium.connect_over_cdp(f"http://127.0.0.1:{CDP_PORT}")
        ctx = browser.contexts[0] if browser.contexts else await browser.new_context()
        page = ctx.pages[0] if ctx.pages else await ctx.new_page()
        await page.goto(LOCAL_URL, wait_until="networkidle", timeout=15000)
        await page.wait_for_timeout(1000)
        result = await measure_coalesced(page)
        print(json.dumps(result, indent=2) if result else "NO SAMPLES")
        await browser.close()
    proc.terminate()
    time.sleep(0.5)
    if proc.poll() is None:
        proc.kill()
    return result


async def main():
    vanilla = await run_vanilla()
    print()
    cdp = await run_cdp()
    print()

    print("=" * 70)
    print("VERDICT")
    print("=" * 70)
    if not vanilla or not cdp:
        print("FAILED: missing results")
        return 3

    v = vanilla
    c = cdp
    print(f"  Vanilla:  count={v['count']:>3}  max={v['max']}  avg={v['avg']:.2f}  median={v['median']}  ones={v['ones']}  2+={v['twos_plus']}")
    print(f"  CDP:      count={c['count']:>3}  max={c['max']}  avg={c['avg']:.2f}  median={c['median']}  ones={c['ones']}  2+={c['twos_plus']}")
    print()

    # Pass: CDP max<=1 while vanilla max>1 (per WebDecoy claim)
    # OR: vanilla avg significantly higher than CDP avg
    cdp_single_entry = c["max"] <= 1
    vanilla_multi_entry = v["max"] > 1

    if cdp_single_entry and vanilla_multi_entry:
        print("🎯 SIGNAL VALIDATED: CDP max<=1, vanilla max>1 → ship it")
        return 0
    elif not cdp_single_entry and vanilla_multi_entry:
        print("⚠️  Both produce multi-entry batches. CDP Input.dispatchMouseEvent DOES coalesce.")
        print("   Spec claim is FALSE on this Chrome version. Reject signal.")
        return 1
    elif cdp_single_entry and not vanilla_multi_entry:
        print("⚠️  Both produce single-entry batches. Vanilla headless doesn't coalesce either.")
        print("   Signal is universal headless = noise. Reject.")
        return 2
    else:
        print("❌ Both single-entry. Signal is non-discriminating. Reject.")
        return 4


if __name__ == "__main__":
    sys.exit(asyncio.run(main()))
