#!/usr/bin/env python3
import contextlib
import json
import pathlib
import socket
import subprocess
import tempfile
import time
import urllib.request
import websocket

ROOT = pathlib.Path(__file__).resolve().parents[1]
GUARD = (ROOT / 'page-guard.js').read_text(encoding='utf-8')


def free_port():
    with socket.socket() as sock:
        sock.bind(('127.0.0.1', 0))
        return sock.getsockname()[1]


class CDP:
    def __init__(self, ws_url):
        self.ws = websocket.create_connection(ws_url, timeout=60, origin='http://localhost')
        self.next_id = 1

    def call(self, method, params=None, timeout=30):
        ident = self.next_id
        self.next_id += 1
        self.ws.send(json.dumps({'id': ident, 'method': method, 'params': params or {}}))
        deadline = time.time() + timeout
        while time.time() < deadline:
            message = json.loads(self.ws.recv())
            if message.get('id') == ident:
                if 'error' in message:
                    raise RuntimeError(message['error'])
                return message.get('result', {})
        raise TimeoutError(method)

    def eval(self, expression, user_gesture=False, timeout=30):
        response = self.call('Runtime.evaluate', {
            'expression': expression,
            'returnByValue': True,
            'awaitPromise': True,
            'userGesture': user_gesture
        }, timeout=timeout)
        if response.get('exceptionDetails'):
            raise RuntimeError(response['exceptionDetails'])
        result = response.get('result', {})
        if result.get('subtype') == 'error':
            raise RuntimeError(result.get('description'))
        return result.get('value')

    def close(self):
        self.ws.close()


def get_page(debug_port):
    endpoint = f'http://127.0.0.1:{debug_port}/json/list'
    for _ in range(200):
        try:
            with urllib.request.urlopen(endpoint, timeout=1) as response:
                targets = json.load(response)
            page = next((target for target in targets if target.get('type') == 'page'), None)
            if page:
                return page
        except Exception:
            pass
        time.sleep(0.1)
    raise RuntimeError('DevTools page unavailable')


def run_context(referrer, expression):
    debug_port = free_port()
    with tempfile.TemporaryDirectory(prefix='grax149-runtime-') as profile:
        command = [
            'chromium', '--headless=new', '--no-sandbox', '--disable-gpu', '--no-first-run',
            '--no-default-browser-check', '--disable-background-networking', '--no-proxy-server',
            '--remote-allow-origins=*', '--autoplay-policy=no-user-gesture-required', '--mute-audio',
            f'--remote-debugging-port={debug_port}', f'--user-data-dir={profile}', 'about:blank'
        ]
        process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
        cdp = None
        try:
            page = get_page(debug_port)
            cdp = CDP(page['webSocketDebuggerUrl'])
            cdp.call('Runtime.enable')
            cdp.call('Page.enable')
            prelude = f'''(() => {{
              try {{ Object.defineProperty(document, 'referrer', {{ configurable: true, value: {json.dumps(referrer)} }}); }} catch {{}}
              window.__nativeOpenCalls = [];
              window.__nativeOpenSpy = function nativeOpenSpy(url, target, features) {{
                window.__nativeOpenCalls.push([String(url ?? ''), String(target ?? ''), String(features ?? '')]);
                return {{ native: true, location: {{ href: String(url || 'about:blank') }}, close() {{}}, focus() {{}} }};
              }};
              window.__nativeWorkerCtor = window.Worker;
              window.__nativeAttachShadow = Element.prototype.attachShadow;
              window.__nativeAddEventListener = EventTarget.prototype.addEventListener;
              window.__nativeInnerHTMLSetter = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML')?.set;
              window.__nativeCreateHTMLDocument = DOMImplementation.prototype.createHTMLDocument;
              try {{ Object.defineProperty(window, 'open', {{ configurable: true, enumerable: true, writable: true, value: window.__nativeOpenSpy }}); }} catch {{ window.open = window.__nativeOpenSpy; }}
            }})();'''
            cdp.call('Page.addScriptToEvaluateOnNewDocument', {'source': prelude})
            cdp.call('Page.addScriptToEvaluateOnNewDocument', {'source': GUARD})
            cdp.call('Page.reload', {'ignoreCache': True}, timeout=60)
            deadline = time.time() + 20
            while time.time() < deadline:
                try:
                    if cdp.eval("document.readyState === 'complete' && Boolean(globalThis.__graxPageGuardInstalled)"):
                        break
                except Exception:
                    pass
                time.sleep(0.1)
            else:
                raise RuntimeError('Guard did not initialize')
            return cdp.eval(expression, user_gesture=True, timeout=60)
        finally:
            if cdp:
                with contextlib.suppress(Exception):
                    cdp.close()
            process.terminate()
            with contextlib.suppress(subprocess.TimeoutExpired):
                process.wait(timeout=5)
            if process.poll() is None:
                process.kill()


def require(result, keys, label):
    missing = [key for key in keys if result.get(key) is not True]
    if missing:
        raise AssertionError(f'{label}: {", ".join(missing)}\n{json.dumps(result, indent=2)}')


def main():
    lifecycle = run_context('https://filma24.stream/movie', r'''(async()=>{
      window.__errors=[];
      addEventListener('error', event=>__errors.push(String(event.error||event.message)));
      addEventListener('unhandledrejection', event=>__errors.push(String(event.reason)));

      const before=window.open('https://rcdn-web.com/pre?clickid=149','_blank');
      document.dispatchEvent(new CustomEvent('__grax_guard_state_v1__',{detail:{enabled:false}}));

      let offHandlerRuns=0;
      const offButton=document.createElement('button');
      offButton.onclick=()=>{offHandlerRuns+=1;window.open('https://rcdn-web.com/off?clickid=149','_blank');};
      document.body.appendChild(offButton);
      offButton.click();
      const offRoot=document.createElement('div').attachShadow({mode:'closed'});
      const offAnchor=document.createElement('a');
      offAnchor.href='https://rcdn-web.com/off-anchor?clickid=149';
      offAnchor.target='_blank';
      offAnchor.addEventListener('click',event=>event.preventDefault());
      const offAnchorResult=offAnchor.click();

      let workerResult=null;
      const blob=new Blob(['postMessage("off-worker-ok")'],{type:'text/javascript'});
      const url=URL.createObjectURL(blob);
      const worker=new Worker(url);
      worker.onmessage=event=>{workerResult=event.data;worker.terminate();URL.revokeObjectURL(url);};
      await new Promise(resolve=>setTimeout(resolve,250));

      document.dispatchEvent(new CustomEvent('__grax_guard_state_v1__',{detail:{enabled:true}}));
      const after=window.open('https://rcdn-web.com/post?clickid=149','_blank');
      const onAnchor=document.createElement('a');
      onAnchor.href='https://rcdn-web.com/on-anchor?clickid=149';
      onAnchor.target='_blank';
      const onAnchorResult=onAnchor.click();
      const onRoot=document.createElement('div').attachShadow({mode:'closed'});

      return {
        installed:Boolean(globalThis.__graxPageGuardInstalled),
        beforeBlocked:Boolean(before?.location?.href==='about:blank'),
        offHandlerRuns:offHandlerRuns===1,
        offNativeOpenReached:__nativeOpenCalls.some(call=>call[0].includes('/off?')),
        offClosedShadowPreserved:offRoot.mode==='closed',
        offAnchorDelegated:offAnchorResult!==false,
        workerRestored:workerResult==='off-worker-ok',
        afterBlocked:Boolean(after?.location?.href==='about:blank'),
        onAnchorBlocked:onAnchorResult===false,
        onShadowGuarded:onRoot.mode==='open',
        errors:__errors,
        nativeOpenCalls:__nativeOpenCalls
      };
    })()''')
    require(lifecycle, [
        'installed', 'beforeBlocked', 'offHandlerRuns', 'offNativeOpenReached',
        'offClosedShadowPreserved', 'offAnchorDelegated', 'workerRestored',
        'afterBlocked', 'onAnchorBlocked', 'onShadowGuarded'
    ], 'runtime kill switch')
    if lifecycle.get('errors'):
        raise AssertionError('runtime kill switch errors\n' + json.dumps(lifecycle, indent=2))

    app_shell = run_context('https://dash.cloudflare.com/secure/dashboard', r'''(async()=>{
      window.__errors=[];
      addEventListener('error', event=>__errors.push(String(event.error||event.message)));
      addEventListener('unhandledrejection', event=>__errors.push(String(event.reason)));

      let frameworkSignals=0;
      const app=document.createElement('div');
      app.id='root';
      app.innerHTML='<button id="route">Route</button><section data-shell="ready"></section>';
      document.body.appendChild(app);
      app.addEventListener('grax-app-route',()=>{frameworkSignals+=1;});
      app.dispatchEvent(new CustomEvent('grax-app-route',{bubbles:true}));
      const closedRoot=document.createElement('div').attachShadow({mode:'closed'});
      const detached=document.implementation.createHTMLDocument('dashboard');
      detached.body.innerHTML='<main id="dashboard-shell"></main>';

      let workerResult=null;
      const blob=new Blob(['postMessage("dashboard-worker-ok")'],{type:'text/javascript'});
      const url=URL.createObjectURL(blob);
      const worker=new Worker(url);
      worker.onmessage=event=>{workerResult=event.data;worker.terminate();URL.revokeObjectURL(url);};
      await new Promise(resolve=>setTimeout(resolve,250));

      const adResult=window.open('https://rcdn-web.com/dashboard-ad?clickid=149','_blank');
      const trustedResult=window.open('https://checkout.stripe.com/c/pay/cs_test_149','_blank');
      const adAnchor=document.createElement('a');
      adAnchor.href='https://rcdn-web.com/dashboard-anchor?clickid=149';
      adAnchor.target='_blank';
      const adAnchorResult=adAnchor.click();

      return {
        installed:Boolean(globalThis.__graxPageGuardInstalled),
        windowOpenStillGuarded:window.open!==window.__nativeOpenSpy,
        addEventListenerNative:EventTarget.prototype.addEventListener===window.__nativeAddEventListener,
        attachShadowNative:Element.prototype.attachShadow===window.__nativeAttachShadow,
        innerHTMLNative:Object.getOwnPropertyDescriptor(Element.prototype,'innerHTML')?.set===window.__nativeInnerHTMLSetter,
        createHTMLDocumentNative:DOMImplementation.prototype.createHTMLDocument===window.__nativeCreateHTMLDocument,
        closedShadowPreserved:closedRoot.mode==='closed',
        frameworkSignals:frameworkSignals===1,
        detachedShellPreserved:Boolean(detached.getElementById('dashboard-shell')),
        workerResult:workerResult==='dashboard-worker-ok',
        adWindowBlocked:Boolean(adResult?.location?.href==='about:blank'),
        trustedWindowNative:Boolean(trustedResult?.native),
        adAnchorBlocked:adAnchorResult===false,
        nativeOpenIsolation:__nativeOpenCalls.length===1 && __nativeOpenCalls[0][0].includes('stripe.com'),
        errors:__errors
      };
    })()''')
    require(app_shell, [
        'installed', 'windowOpenStillGuarded', 'addEventListenerNative',
        'attachShadowNative', 'innerHTMLNative', 'createHTMLDocumentNative',
        'closedShadowPreserved', 'frameworkSignals', 'detachedShellPreserved',
        'workerResult', 'adWindowBlocked', 'trustedWindowNative',
        'adAnchorBlocked', 'nativeOpenIsolation'
    ], 'application shell compatibility')
    if app_shell.get('errors'):
        raise AssertionError('application shell errors\n' + json.dumps(app_shell, indent=2))

    print(json.dumps({
        'chromium': subprocess.check_output(['chromium', '--version'], text=True).strip(),
        'runtimeKillSwitch': lifecycle,
        'cloudflareDashboardApplicationShell': app_shell
    }, indent=2))


if __name__ == '__main__':
    main()
