"""CSS / external JavaScript / events / actual Monaco checks in a private QEMU.

The HTTP peer supplies controlled bytes over the guest's native NIC. QMP only
sends real keyboard/mouse input and captures the native framebuffer. No browser
state is injected through a host browser, guest debugger or synthetic DOM.
"""
import hashlib
import html
import json
import pathlib
import re
import threading
import time
import uuid
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlsplit

from PIL import Image
from NativeCSharpEvidence import CSHARP_PROOF, csharp_frame_stats, verify_readback, website_palette


WINDOW = re.compile(r'^XFE_WINDOW_STATE\|(\d+)\|Browser\|(-?\d+)\|(-?\d+)\|(\d+)\|(\d+)\|([01])\|([01])\|([01])$', re.M)
PAGE = re.compile(r'XFE_BROWSER_STATE_BEGIN\r?\n([^\n]*)\n(.*?)\r?\nXFE_BROWSER_STATE_END', re.S)
MODEL = re.compile(r'XFE_MONACO_VALUE_BEGIN\r?\n(.*?)\r?\nXFE_MONACO_VALUE_END', re.S)


class BrowserFixture:
    """Loopback-only fixture with request evidence and deterministic resources."""
    def __init__(self, nonce):
        self.nonce = nonce
        self.requests = []
        self.lock = threading.Lock()
        quoted = json.dumps(nonce)
        self.resources = {
            '/External.css': ('text/css', '''html,body{margin:0;padding:0;background:#F1F6FB;color:#162638;font-family:monospace;font-size:16px}
            #Swatch{position:absolute;left:24px;top:24px;width:160px;height:64px;box-sizing:border-box;border:4px solid #75C9F5;background:#1877C9}
            #Click{position:absolute;left:24px;top:104px;width:200px;height:40px;box-sizing:border-box;background:#4086B8;color:white;border:2px solid #75C9F5}
            #Input{position:absolute;left:24px;top:160px;width:300px;height:60px;box-sizing:border-box;background:white;color:#162638;border:2px solid #4086B8;padding:4px}
            #Outputs{position:absolute;left:360px;top:24px;width:400px;font-size:13px;line-height:20px}'''),
            '/Classic.js': ('application/javascript', "window.fixtureOrder=['classic'];"),
            '/Defer.js': ('application/javascript', '''window.fixtureOrder.push('defer');
            document.getElementById('Ready').textContent='External ready '+NONCE+' '+fixtureOrder.join(',');
            document.getElementById('Click').addEventListener('click',function(){document.getElementById('ClickResult').textContent='Button clicked '+NONCE;});
            document.getElementById('Input').addEventListener('input',function(){document.getElementById('InputResult').textContent='Input typed '+this.value+' '+NONCE;});
            setTimeout(function(){document.getElementById('Timer').textContent='Timer fired '+NONCE;},1200);
            requestAnimationFrame(function(){document.getElementById('Frame').textContent='Animation frame '+NONCE;});
            var external=document.createElement('script');external.src='/Dynamic.js';document.head.appendChild(external);'''.replace('NONCE', quoted)),
            '/Dynamic.js': ('application/javascript', "window.fixtureOrder.push('dynamic');document.getElementById('Dynamic').textContent='Dynamic loaded '+" + quoted + ";"),
        }
        document = '''<!doctype html><html><head><title>XFE pipeline</title>
        <link rel=stylesheet href=/External.css><script src=/Classic.js></script><script defer src=/Defer.js></script></head>
        <body><div id=Swatch></div><button id=Click>Click native event</button><textarea id=Input></textarea>
        <div id=Outputs><div id=Ready>External pending</div><div id=Dynamic>Dynamic pending</div><div id=ClickResult>Click pending</div>
        <div id=InputResult>Input pending</div><div id=Timer>Timer pending</div><div id=Frame>Frame pending</div><div>NONCE</div></div></body></html>'''
        self.resources['/Index'] = ('text/html', document.replace('NONCE', html.escape(nonce)))
        fixture = self

        class Handler(BaseHTTPRequestHandler):
            protocol_version = 'HTTP/1.1'
            def log_message(self, *_):
                pass
            def do_GET(self):
                path = urlsplit(self.path).path
                with fixture.lock:
                    fixture.requests.append({'Path': self.path, 'Utc': datetime.now(timezone.utc).isoformat(),
                                             'Host': self.headers.get('Host'), 'Peer': self.client_address[0]})
                self.close_connection = True
                resource = fixture.resources.get(path)
                if resource is None:
                    self.send_error(404)
                    return
                content_type, source = resource
                body = source.encode('utf-8')
                try:
                    self.send_response(200)
                    self.send_header('Content-Type', content_type + '; charset=utf-8')
                    self.send_header('Content-Length', str(len(body)))
                    self.send_header('Connection', 'close')
                    self.end_headers()
                    self.wfile.write(body)
                    self.wfile.flush()
                except (BrokenPipeError, ConnectionResetError):
                    pass

        self.server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
        self.server.daemon_threads = True
        self.thread = threading.Thread(target=self.server.serve_forever, kwargs={'poll_interval': .1}, daemon=True)
        self.thread.start()

    @property
    def address(self):
        return f'http://10.0.2.2:{self.server.server_port}/Index'

    def close(self):
        self.server.shutdown()
        self.server.server_close()
        self.thread.join(timeout=2)


def _monaco_frame_stats(frame, viewport, scale):
    """Measure real glyph ink in the controlled page's configured 20px lines.

    This does not synthesize or OCR code. The actual getValue assertion verifies
    text; these independent pixel bands verify its multi-line native paint.
    The header, gutter and pointer are kept outside this region by the caller.
    """
    vx, vy, width, _ = viewport
    lines = []
    for index in range(4):
        x, y, w, h = vx + 42, vy + 28 + index * 20, min(600, width - 42), 20
        native = (round(x * scale), round(y * scale), round((x + w) * scale), round((y + h) * scale))
        crop = frame.crop(native).convert('RGB')
        columns, rows = set(), set()
        ink = blue = green = 0
        for row in range(crop.height):
            for column in range(crop.width):
                pixel = crop.getpixel((column, row))
                # The editor's dark background/current-line borders are below
                # this threshold; syntax colors and foreground glyphs exceed it.
                if .2126 * pixel[0] + .7152 * pixel[1] + .0722 * pixel[2] >= 78:
                    ink += 1; columns.add(column); rows.add(row)
                if all(abs(pixel[channel] - (0x56, 0x9C, 0xD6)[channel]) <= 12 for channel in range(3)):
                    blue += 1
                if any(all(abs(pixel[channel] - color[channel]) <= 12 for channel in range(3)) for color in ((0x60, 0x8B, 0x4E), (0x6A, 0x99, 0x55))):
                    green += 1
        span = max(columns) - min(columns) + 1 if columns else 0
        glyphs = ink >= 35 * scale * scale and len(columns) >= 24 * scale and len(rows) >= 4 * scale and span >= 40 * scale
        lines.append({'Line': index + 1, 'CssBounds': [x, y, w, h], 'NativeBounds': list(native),
                      'InkPixels': ink, 'InkColumns': len(columns), 'InkRows': len(rows), 'InkSpan': span,
                      'BlueSyntaxPixels': blue, 'GreenCommentPixels': green, 'HasTextGlyphDistribution': glyphs})
    return {'ConfiguredLineHeightCss': 20, 'EditorHeaderHeightCss': 28, 'Lines': lines,
            'ReadyPainted': all(line['HasTextGlyphDistribution'] for line in lines[:3])
                and lines[0]['GreenCommentPixels'] >= 8 * scale * scale
                and lines[1]['BlueSyntaxPixels'] >= 8 * scale * scale}


def _monaco_fourth_line_delta(before, after, native_bounds):
    """New text ink only: changing a dark selection/current-line fill is insufficient."""
    old = before.crop(native_bounds).convert('RGB'); new = after.crop(native_bounds).convert('RGB')
    added = set(); pixels = 0
    for row in range(new.height):
        for column in range(new.width):
            old_pixel = old.getpixel((column, row)); pixel = new.getpixel((column, row))
            old_ink = .2126 * old_pixel[0] + .7152 * old_pixel[1] + .0722 * old_pixel[2] >= 78
            new_ink = .2126 * pixel[0] + .7152 * pixel[1] + .0722 * pixel[2] >= 78
            if new_ink and not old_ink:
                pixels += 1; added.add(column)
    return {'NewInkPixels': pixels, 'NewInkColumns': len(added)}


def run_browser_checks(qmp, text, wait_log, screenshot, native_width, native_height, scale, pointer):
    checks = []
    evidence = {'Kind': 'NativeBrowserPipeline', 'NativeWidth': native_width, 'NativeHeight': native_height,
                'Scale': scale, 'Checks': checks, 'Passed': False,
                'Boundary': 'Original bounded CSS layout and classic-script interpreter; no full Chromium, complete CSS, workers, GPU canvas or verified HTTPS claim.'}
    nonce = 'Guest-' + uuid.uuid4().hex[:12]
    fixture = BrowserFixture(nonce)
    output = None

    def key(code, down):
        qmp('input-send-event', {'events': [{'type': 'key', 'data': {'down': down, 'key': {'type': 'qcode', 'data': code}}}]})
        time.sleep(.025)

    def tap(code):
        key(code, True); key(code, False)

    def chord(modifier, code):
        key(modifier, True); tap(code); key(modifier, False)

    def type_text(value):
        simple = {'.': 'dot', '/': 'slash', '-': 'minus', ' ': 'spc', '\n': 'ret'}
        shifted = {':': 'semicolon', '_': 'minus', '?': 'slash'}
        for character in value:
            if character in shifted: chord('shift', shifted[character])
            elif character.isupper(): chord('shift', character.lower())
            else: tap(simple.get(character, character))

    def move(x, y):
        target = [round(x * scale), round(y * scale)]
        while pointer != target:
            dx = max(-80, min(80, target[0] - pointer[0])); dy = max(-80, min(80, target[1] - pointer[1]))
            qmp('input-send-event', {'events': [{'type': 'rel', 'data': {'axis': 'x', 'value': dx}}, {'type': 'rel', 'data': {'axis': 'y', 'value': dy}}]})
            pointer[0] += dx; pointer[1] += dy
            time.sleep(.018)

    def click(x, y):
        move(x, y)
        for down in (True, False):
            qmp('input-send-event', {'events': [{'type': 'btn', 'data': {'down': down, 'button': 'left'}}]})
            time.sleep(.08)

    def capture(name):
        nonlocal output
        time.sleep(.15)
        path = pathlib.Path(screenshot(name)).resolve()
        output = path.parent
        with Image.open(path) as saved:
            assert saved.size == (native_width, native_height), saved.size
            frame = saved.convert('RGB').copy()
        evidence.setdefault('Screenshots', []).append({'Path': str(path), 'Sha256': hashlib.sha256(path.read_bytes()).hexdigest()})
        return frame

    def page_bodies(start):
        return [match.group(2).replace('\r', '') for match in PAGE.finditer(text()[start:])]

    def wait_body(marker, start, timeout=20):
        deadline = time.monotonic() + timeout
        while time.monotonic() < deadline:
            bodies = page_bodies(start)
            for body in reversed(bodies):
                if marker in body: return body
            log = text()
            assert '[FAILFAST]' not in log and '*** UNHANDLED EXCEPTION' not in log and 'CPU EXCEPTION:' not in log, log[-4000:]
            time.sleep(.05)
        raise AssertionError('Native browser state timeout: ' + marker + '\n' + text()[start:][-7000:])

    def navigate(address):
        start = len(text()); chord('ctrl', 'l'); type_text(address); tap('ret')
        wait_log('XFE_BROWSER_NAVIGATED|', start, 30)
        return start

    def browser_bounds(start):
        deadline = time.monotonic() + 10
        while time.monotonic() < deadline:
            matches = list(WINDOW.finditer(text()[start:]))
            if matches:
                parts = matches[-1].groups()
                return {'Id': int(parts[0]), 'X': int(parts[1]), 'Y': int(parts[2]), 'Width': int(parts[3]), 'Height': int(parts[4])}
            time.sleep(.05)
        raise AssertionError('Browser window geometry log missing')

    def count_color(frame, bounds, rgb, tolerance=3):
        x, y, width, height = bounds
        crop = frame.crop((round(x * scale), round(y * scale), round((x + width) * scale), round((y + height) * scale)))
        return sum(all(abs(pixel[channel] - rgb[channel]) <= tolerance for channel in range(3)) for pixel in crop.getdata())

    try:
        start = len(text()); click(60, 308)
        wait_log('XFE_DESKTOP_ACTIVE|Browser', start)
        window = browser_bounds(start)
        viewport = (window['X'] + 2, window['Y'] + 67, window['Width'] - 4, window['Height'] - 96)
        evidence['BrowserWindow'] = window; evidence['BrowserViewport'] = viewport
        capture('Browser-Pipeline-Start')  # Establish the private evidence directory even if a later request fails.
        wait_log('XFE_NETWORK_DHCP_OK|10.0.2.15|DnsServers|1', timeout=20)
        start = navigate(fixture.address)
        ready = wait_body('External ready ' + nonce + ' classic,defer', start)
        wait_body('Dynamic loaded ' + nonce, start)
        checks.append('native HTTP external CSS and ordered classic/defer/dynamic JavaScript')
        evidence['ExternalReadyBody'] = ready

        move(window['X'] + window['Width'] - 20, window['Y'] + 50)
        frame = capture('Browser-Pipeline-Css')
        vx, vy, _, _ = viewport
        color_pixels = count_color(frame, (vx + 30, vy + 30, 148, 52), (0x18, 0x77, 0xC9))
        border_pixels = count_color(frame, (vx + 25, vy + 25, 158, 2), (0x75, 0xC9, 0xF5))
        outside_pixels = count_color(frame, (vx + 188, vy + 30, 10, 30), (0xF1, 0xF6, 0xFB))
        assert color_pixels > 148 * 52 * scale * scale * .95, ('CSS fill geometry/color', color_pixels)
        assert border_pixels > 158 * 2 * scale * scale * .8, ('CSS border geometry/color', border_pixels)
        assert outside_pixels > 10 * 30 * scale * scale * .95, ('CSS width extends beyond declared border-box', outside_pixels)
        evidence['CssPixels'] = {'Fill': color_pixels, 'Border': border_pixels, 'Outside': outside_pixels}
        checks.append('native framebuffer external CSS box geometry, fill and border colors')

        start = len(text()); click(vx + 90, vy + 124)
        wait_body('Button clicked ' + nonce, start)
        capture('Browser-Pipeline-Click')
        checks.append('native mouse hit-test and DOM click listener mutate visible content')

        start = len(text()); click(vx + 65, vy + 180)
        typed = 'xfenativeinput'
        type_text(typed); wait_body('Input typed ' + typed + ' ' + nonce, start)
        input_frame = capture('Browser-Pipeline-Input')
        ink = count_color(input_frame, (vx + 30, vy + 166, 270, 24), (0x16, 0x26, 0x38), 10)
        assert ink > 12 * scale * scale, ('textarea current value has no native text ink', ink)
        evidence['TypedValue'] = typed; evidence['InputInkPixels'] = ink
        checks.append('native keyboard beforeinput/input events and current textarea value paint')

        wait_body('Timer fired ' + nonce, start=0)
        wait_body('Animation frame ' + nonce, start=0)
        checks.append('guest monotonic timer and requestAnimationFrame callbacks update DOM')

        start = navigate('about:Monaco')
        ready = wait_body('Official Monaco 0.52.2 - ready', start, 90)
        # A theme-colored antialiased pointer can resemble a syntax color.
        # Keep it on the address toolbar, outside every code band, before capture.
        pointer_away = (window['X'] + window['Width'] - 20, window['Y'] + 50)
        move(*pointer_away)
        evidence['MonacoPointerExcluded'] = {'LogicalPosition': list(pointer_away), 'OutsideCodeBands': True}
        # Editor construction precedes its first RAF text render. Wait for
        # actual pixels rather than treating the model-ready string as paint.
        paint_deadline = time.monotonic() + 60
        paint_attempt = 0
        while True:
            paint_attempt += 1
            frame = capture('Browser-OfficialMonaco-Ready-' + str(paint_attempt))
            ready_paint = _monaco_frame_stats(frame, viewport, scale)
            evidence['MonacoReadyPaint'] = {'Attempts': paint_attempt, **ready_paint}
            if ready_paint['ReadyPainted'] or time.monotonic() >= paint_deadline: break
            assert all(marker not in text() for marker in ('CPU EXCEPTION:', '[FAILFAST]', '*** UNHANDLED EXCEPTION')), text()[-4000:]
            time.sleep(.25)
        model_matches = list(MODEL.finditer(text()[start:]))
        assert model_matches, 'Ready marker without actual Monaco model diagnostic'
        original = model_matches[-1].group(1).replace('\r', '')
        assert 'const answer = 6 * 7;' in original and 'console.log(answer);' in original, original
        assert ready_paint['ReadyPainted'], ('Monaco model ready without three native code glyph lines and their syntax-color distribution', ready_paint)
        token_pixels = sum(line['BlueSyntaxPixels'] for line in ready_paint['Lines'][:3])
        evidence['MonacoReadyBody'] = ready; evidence['MonacoInitialValue'] = original; evidence['MonacoSyntaxPixels'] = token_pixels
        checks.append('official packaged Monaco 0.52.2 initializes actual model and three native glyph lines with comment/keyword syntax-color distribution outside the pointer')
        ready_frame = frame

        start = len(text()); click(vx + 160, vy + 70)
        chord('ctrl', 'end'); tap('ret')
        marker = 'xfenativeedit'
        type_text(marker)
        wait_body('Monaco - edited', start, 30)
        deadline = time.monotonic() + 20
        edited = ''
        while time.monotonic() < deadline:
            models = list(MODEL.finditer(text()[start:]))
            if models:
                edited = models[-1].group(1).replace('\r', '')
                if marker in edited: break
            time.sleep(.05)
        assert marker in edited and edited != original, ('Native typing did not update real Monaco getValue()', edited)
        assert edited == original + '\n' + marker, ('Native input was lost, duplicated or inserted at the wrong position', edited)
        original_lines, edited_lines = original.split('\n'), edited.split('\n')
        assert len(original_lines) == 3 and len(edited_lines) >= 4 and edited_lines[:3] == original_lines and marker in edited_lines[3], \
            ('Native Ctrl+End/newline typing did not produce the expected real fourth model line', edited)
        evidence['MonacoEditedValue'] = edited
        checks.append('native mouse/keyboard edits official Monaco model confirmed by actual getValue')
        move(*pointer_away)
        paint_deadline = time.monotonic() + 60
        edit_attempt = 0
        while True:
            edit_attempt += 1
            edited_frame = capture('Browser-OfficialMonaco-Edited-' + str(edit_attempt))
            edited_paint = _monaco_frame_stats(edited_frame, viewport, scale)
            delta = _monaco_fourth_line_delta(ready_frame, edited_frame, edited_paint['Lines'][3]['NativeBounds'])
            visible = edited_paint['ReadyPainted'] and edited_paint['Lines'][3]['HasTextGlyphDistribution'] \
                and delta['NewInkPixels'] >= 35 * scale * scale and delta['NewInkColumns'] >= 24 * scale
            evidence['MonacoEditedPaint'] = {'Attempts': edit_attempt, **edited_paint, 'FourthLineDelta': delta, 'FourthModelLine': edited_lines[3], 'Passed': visible}
            if visible or time.monotonic() >= paint_deadline: break
            assert all(marker not in text() for marker in ('CPU EXCEPTION:', '[FAILFAST]', '*** UNHANDLED EXCEPTION')), text()[-4000:]
            time.sleep(.25)
        assert visible, ('Real Monaco model changed but its native fourth-line text ink did not appear', evidence['MonacoEditedPaint'])
        capture('Browser-OfficialMonaco-Edited')  # Keep the established final artifact name as well as every waited frame.
        checks.append('native fourth-line text glyphs and new ink agree with the real edited Monaco model after RAF paint')

        # A real page button selects the actual C# model. Host probes and
        # generated palette files never stand in for this guest interaction.
        palette = website_palette()
        csharp = {'Kind': 'ActualNativeMonacoWebsiteCSharp', 'Passed': False,
                  'WebsiteAssetManifestSha256': palette['AssetManifestSha256'],
                  'WebsiteCssSha256': palette['CssSha256'], 'MemoryProfile': 'Caller records the actual QEMU memory setting',
                  'Input': 'QMP physical mouse/keyboard events; no injected guest JavaScript or synthetic model',
                  'Boundary': 'Website grammar/CSS foreground parity; no Roslyn, C# execution, workers or pixel-identical font claim.'}
        evidence['MonacoCSharp'] = csharp

        def wait_csharp(start, marker=None, timeout=90):
            deadline = time.monotonic() + timeout
            last_error = 'No complete native C# DOM readback appeared'
            while time.monotonic() < deadline:
                for match in reversed(list(CSHARP_PROOF.finditer(text()[start:]))):
                    try:
                        actual = json.loads(match.group(1))
                        if actual.get('Language') != 'csharp':
                            continue
                        if marker is not None and marker not in actual.get('ModelValue', ''):
                            continue
                        verified = verify_readback(actual, palette)
                        return actual, verified
                    except (AssertionError, KeyError, TypeError, ValueError) as error:
                        last_error = str(error)
                        # Report the newest matching guest state. Rechecking an
                        # older pre-RAF snapshot would overwrite this diagnostic
                        # with an obsolete text mismatch and hide the real
                        # remaining token-color failure.
                        break
                assert all(item not in text() for item in ('CPU EXCEPTION:', '[FAILFAST]', '*** UNHANDLED EXCEPTION')), text()[-4000:]
                time.sleep(.1)
            raise AssertionError('Actual native C# model/span color readback did not stabilize: ' + last_error)

        def wait_csharp_input(start, expected_model, line_number, column, action, timeout=30):
            """Observe actual textarea focus/cursor/model after physical input.

            No key is resent, no focus API is called and no model is injected.
            A lost or misplaced character is a failure at its exact stage.
            """
            deadline = time.monotonic() + timeout
            last = None
            while time.monotonic() < deadline:
                matches = list(CSHARP_PROOF.finditer(text()[start:]))
                if matches:
                    last = json.loads(matches[-1].group(1))
                    position = last.get('CursorPosition') or {}
                    if (last.get('Language') == 'csharp' and last.get('ModelValue') == expected_model and
                            last.get('EditorHasTextFocus') is True and last.get('ActiveElementTag') == 'TEXTAREA' and
                            'inputarea' in (last.get('ActiveElementClasses') or '').split() and
                            position.get('LineNumber') == line_number and position.get('Column') == column):
                        acknowledgement = {key:last.get(key) for key in ('Language','ModelValue','ModelVersionId','ActiveElementTag',
                            'ActiveElementClasses','ActiveElementId','EditorHasTextFocus','EditorHasWidgetFocus','CursorPosition','Selection')}
                        acknowledgement.update(Action=action,SerialOffset=start+matches[-1].start())
                        csharp.setdefault('NativeInputAcknowledgements',[]).append(acknowledgement)
                        return last
                assert all(item not in text() for item in ('CPU EXCEPTION:', '[FAILFAST]', '*** UNHANDLED EXCEPTION')), text()[-4000:]
                time.sleep(.05)
            raise AssertionError(('Physical native C# input was not acknowledged', action,
                                  {'ModelValue':expected_model,'LineNumber':line_number,'Column':column},last))

        def wait_csharp_focus(start, expected_model, timeout=30):
            deadline = time.monotonic() + timeout
            last = None
            while time.monotonic() < deadline:
                matches = list(CSHARP_PROOF.finditer(text()[start:]))
                if matches:
                    last = json.loads(matches[-1].group(1))
                    if (last.get('Language') == 'csharp' and last.get('ModelValue') == expected_model and
                            last.get('EditorHasTextFocus') is True and last.get('ActiveElementTag') == 'TEXTAREA' and
                            'inputarea' in (last.get('ActiveElementClasses') or '').split()):
                        csharp['PhysicalClickFocusAcknowledgement'] = {key:last.get(key) for key in ('ActiveElementTag',
                            'ActiveElementClasses','ActiveElementId','EditorHasTextFocus','EditorHasWidgetFocus','CursorPosition','ModelVersionId')}
                        csharp['PhysicalClickFocusAcknowledgement']['SerialOffset'] = start+matches[-1].start()
                        return last
                assert all(item not in text() for item in ('CPU EXCEPTION:', '[FAILFAST]', '*** UNHANDLED EXCEPTION')), text()[-4000:]
                time.sleep(.05)
            raise AssertionError(('Physical C# editor click did not focus the actual Monaco textarea',last))

        csharp_start = len(text())
        click(vx + viewport[2] - 20, vy + 14)
        csharp['LanguageButtonCssPosition'] = [vx + viewport[2] - 20, vy + 14]
        wait_body('Monaco C# - website grammar and palette', csharp_start, 90)
        actual, verified = wait_csharp(csharp_start)
        csharp_original = actual['ModelValue']
        assert csharp_original.startswith('using System;\nnamespace XFEstudio;') and 'public sealed class Preview' in csharp_original, csharp_original
        assert 'Math.Max(value, 42)' in csharp_original and 'Console.WriteLine("Ready")' in csharp_original
        csharp['InitialModelValue'] = csharp_original
        csharp['InitialDomReadback'] = actual
        csharp['InitialSpanColorParity'] = verified
        checks.append('real native C# language button selects the official Monaco model with website highlight.js token classification')
        checks.append('every actual native C# rendered span foreground agrees with the unchanged website CSS palette and token scopes')
        move(*pointer_away)
        paint_deadline = time.monotonic() + 60
        attempt = 0
        while True:
            attempt += 1
            csharp_frame = capture('Browser-OfficialMonaco-CSharp-Ready-' + str(attempt))
            painted = csharp_frame_stats(csharp_frame, viewport, scale, csharp_original)
            csharp['InitialFramebuffer'] = {'Attempts': attempt, **painted}
            if painted['Passed'] or time.monotonic() >= paint_deadline:
                break
            assert all(item not in text() for item in ('CPU EXCEPTION:', '[FAILFAST]', '*** UNHANDLED EXCEPTION')), text()[-4000:]
            time.sleep(.25)
        assert painted['Passed'], ('Actual C# DOM colors were correct but the real native framebuffer did not paint the C# source and all website categories', painted)
        capture('Browser-OfficialMonaco-CSharp-Ready')
        checks.append('native C# framebuffer shows actual model lines and website keyword/type/control-flow/string/number foreground ink outside pointer and UI')

        csharp_edit_start = len(text())
        move(vx + 160, vy + 70)
        hold_started = time.monotonic()
        hold_evidence = {'BrowserCssPosition':[160,70],'DesktopCssPosition':[vx+160,vy+70],
                         'NativePixelPosition':[round((vx+160)*scale),round((vy+70)*scale)],
                         'MouseDownSerialOffset':len(text()),'MouseDownSent':False,'MouseUpSent':False,
                         'FocusAcknowledged':False,'Retries':0,'FocusOrModelInjected':False}
        csharp['HoldUntilActualGuestFocus'] = hold_evidence
        qmp('input-send-event',{'events':[{'type':'btn','data':{'down':True,'button':'left'}}]})
        hold_evidence['MouseDownSent'] = True
        try:
            focus_actual = wait_csharp_focus(hold_evidence['MouseDownSerialOffset'],csharp_original)
            hold_evidence['FocusAcknowledged'] = True
            hold_evidence['FocusedCursorPosition'] = focus_actual['CursorPosition']
            hold_evidence['UnchangedModelLength'] = focus_actual['ModelLength']
            actual_mousedowns = re.findall(r'XFE_BROWSER_INPUT\|mousedown\|160\|70\|[^\r\n]*',text()[hold_evidence['MouseDownSerialOffset']:])
            assert actual_mousedowns, 'Physical C# mouse-down did not reach the actual editor body'
            hold_evidence['ActualBodyMouseDownTrace'] = actual_mousedowns[-1]
        finally:
            # Always release the real device on a failed focus assertion too.
            # Holding until observed focus is not a repeated click or JS focus.
            release_start = len(text())
            qmp('input-send-event',{'events':[{'type':'btn','data':{'down':False,'button':'left'}}]})
            hold_evidence['MouseUpSent'] = True
            hold_evidence['MouseUpSerialOffset'] = release_start
            hold_evidence['HeldHostSeconds'] = time.monotonic()-hold_started
        wait_log('XFE_BROWSER_INPUT|mouseup|160|70|',release_start,30)
        actual_mouseups = re.findall(r'XFE_BROWSER_INPUT\|mouseup\|160\|70\|[^\r\n]*',text()[release_start:])
        assert actual_mouseups, 'Physical C# mouse-up was not routed to the actual editor body'
        hold_evidence['ActualBodyMouseUpTrace'] = actual_mouseups[-1]
        native_transitions = re.findall(r'XFE_NATIVE_POINTER\|[^\r\n]*',text()[hold_evidence['MouseDownSerialOffset']:])
        hold_evidence['ActualNativePointerTransitions'] = native_transitions
        end_start = len(text());chord('ctrl', 'end')
        original_lines = csharp_original.split('\n')
        wait_csharp_input(end_start,csharp_original,len(original_lines),len(original_lines[-1])+1,'Ctrl+End reaches actual model end')
        enter_start = len(text());tap('ret')
        wait_csharp_input(enter_start,csharp_original+'\n',len(original_lines)+1,1,'Enter creates actual final model line')
        csharp_marker = '// xfenativecsharp'
        for index, character in enumerate(csharp_marker):
            character_start = len(text());type_text(character)
            wait_csharp_input(character_start,csharp_original+'\n'+csharp_marker[:index+1],len(original_lines)+1,index+2,
                              'Physical C# character '+str(index+1)+' '+repr(character))
        wait_body('Monaco - edited', csharp_edit_start, 30)
        edited_actual, edited_verified = wait_csharp(csharp_edit_start, csharp_marker)
        assert edited_actual['ModelValue'] == csharp_original + '\n' + csharp_marker, ('C# native model input was lost, duplicated or inserted at an unexpected position', edited_actual['ModelValue'])
        csharp['EditedModelValue'] = edited_actual['ModelValue']
        csharp['EditedDomReadback'] = edited_actual
        csharp['EditedSpanColorParity'] = edited_verified
        move(*pointer_away)
        paint_deadline = time.monotonic() + 60
        attempt = 0
        while True:
            attempt += 1
            edited_csharp_frame = capture('Browser-OfficialMonaco-CSharp-Edited-' + str(attempt))
            edited_csharp_paint = csharp_frame_stats(edited_csharp_frame, viewport, scale, edited_actual['ModelValue'], require_comment=True)
            csharp['EditedFramebuffer'] = {'Attempts': attempt, **edited_csharp_paint}
            if edited_csharp_paint['Passed'] or time.monotonic() >= paint_deadline:
                break
            assert all(item not in text() for item in ('CPU EXCEPTION:', '[FAILFAST]', '*** UNHANDLED EXCEPTION')), text()[-4000:]
            time.sleep(.25)
        assert edited_csharp_paint['Passed'], ('Edited C# actual model/newline/comment did not paint with the website comment color', edited_csharp_paint)
        capture('Browser-OfficialMonaco-CSharp-Edited')
        csharp['Passed'] = True
        checks.append('physical native C# typing appends a real new model line and repaints every span with website colors including the new comment')
        requested = [urlsplit(item['Path']).path for item in fixture.requests]
        for expected in ('/Index', '/External.css', '/Classic.js', '/Defer.js', '/Dynamic.js'):
            assert expected in requested, ('missing native peer request', expected, requested)
        evidence['FixtureNonce'] = nonce; evidence['FixtureAddress'] = fixture.address
        evidence['FixtureRequests'] = fixture.requests.copy()
        evidence['FixtureResourcesSha256'] = {path: hashlib.sha256(source.encode('utf-8')).hexdigest() for path, (_, source) in fixture.resources.items()}
        evidence['Passed'] = True
        return evidence
    except Exception as error:
        evidence['Failure'] = str(error)
        raise
    finally:
        fixture.close()
        if output is not None:
            (output / 'BrowserPipelinePeer.json').write_text(json.dumps({'Nonce': nonce, 'Address': fixture.address, 'Requests': fixture.requests}, ensure_ascii=False, indent=2), encoding='utf-8')
            evidence['FixtureNonce'] = nonce; evidence['FixtureAddress'] = fixture.address; evidence['FixtureRequests'] = fixture.requests.copy()
            evidence['FixtureResourcesSha256'] = {path: hashlib.sha256(source.encode('utf-8')).hexdigest() for path, (_, source) in fixture.resources.items()}
            # The on-disk receipt also survives an assertion failure, including
            # all original screenshot hashes and the final native paint measurements.
            receipt_path = output / 'BrowserPipelineEvidence.json'
            receipt_path.write_text(json.dumps(evidence, ensure_ascii=False, indent=2), encoding='utf-8')
            evidence['EvidencePath'] = str(receipt_path)
            evidence['EvidenceSha256'] = hashlib.sha256(receipt_path.read_bytes()).hexdigest()
