#!/usr/bin/env python3
"""v146 persistent live TCP/SPTI completion-surface bridge.

Boots GDR-8050L firmware once and feeds a persistent command queue. The queue
starts as raw SCSI/MMC CDB script entries, is resolved by gdr_scsi_command_parser.py
for firmware-exact validation, and is also replayed through the stateful JSONL
host bridge used as a command-server/iSCSI prototype.
"""
from __future__ import annotations

import argparse, json, os, subprocess, sys
from pathlib import Path
from typing import Any

from gdr_scsi_command_parser import GDRScsiParser, hx

WORKSPACE = os.environ.get('XDVD_WORKSPACE', r'D:\vscode-workspace\xbox-dvd-tools-v2')
FWBIN = os.environ.get('GDR8050L_FWBIN', r'D:\vscode-workspace\xbox-dvd-tools-v2\firmware\GDR-8050L\8050L_fw_plaintext_256k.bin')


def run_checked(cmd: list[str], cwd: Path, stdout_file: Path | None = None) -> None:
    if stdout_file:
        with stdout_file.open('w', encoding='utf-8', errors='replace') as fh:
            res = subprocess.run(cmd, cwd=str(cwd), stdout=fh, stderr=subprocess.STDOUT)
    else:
        res = subprocess.run(cmd, cwd=str(cwd))
    if res.returncode != 0:
        raise SystemExit(res.returncode)


def minimal_watchpoints() -> list[dict[str, str]]:
    return [
        {'start': '0x0000DF70', 'size': '0x40', 'kind': 'write', 'name': 'watch_df70'},
        {'start': '0x0000DF90', 'size': '0x08', 'kind': 'read',  'name': 'watch_df90'},
        {'start': '0x0000DF2A', 'size': '0x01', 'kind': 'write', 'name': 'watch_df2a'},
        {'start': '0x0000D108', 'size': '0x02', 'kind': 'access','name': 'watch_D108_data_phase'},
        {'start': '0x000005CD', 'size': '0x02', 'kind': 'access','name': 'watch_05CD_auth_candidate'},
    ]


def debug_watchpoints() -> list[dict[str, str]]:
    return minimal_watchpoints() + [
        {'start': '0x000005B1', 'size': '0x01', 'kind': 'access', 'name': 'watch_05B1'},
        {'start': '0x00000143', 'size': '0x01', 'kind': 'access', 'name': 'watch_0143'},
        {'start': '0x0000014A', 'size': '0x01', 'kind': 'access', 'name': 'watch_014A'},
        {'start': '0x000000DE', 'size': '0x01', 'kind': 'access', 'name': 'watch_00DE_host_gate'},
        {'start': '0x0000016D', 'size': '0x01', 'kind': 'access', 'name': 'watch_016D_host_gate'},
        {'start': '0x000008A8', 'size': '0x08', 'kind': 'access', 'name': 'watch_08A8_service_state'},
        {'start': '0x000007AC', 'size': '0x04', 'kind': 'access', 'name': 'watch_07AC_packet_state'},
        {'start': '0x00000778', 'size': '0x01', 'kind': 'access', 'name': 'watch_0778_dispatch'},
        {'start': '0x000007E8', 'size': '0x08', 'kind': 'access', 'name': 'watch_07E8_cdb_mirror'},
        {'start': '0x0000D950', 'size': '0x08', 'kind': 'access', 'name': 'watch_D950_packet_status'},
        {'start': '0x0000DA18', 'size': '0x01', 'kind': 'access', 'name': 'watch_DA18_packet_branch'},
        {'start': '0x00000278', 'size': '0x01', 'kind': 'access', 'name': 'watch_0278_media_branch'},
        {'start': '0x000005B6', 'size': '0x02', 'kind': 'access', 'name': 'watch_05B6_scheduler'},
        {'start': '0x000006F1', 'size': '0x03', 'kind': 'access', 'name': 'watch_06F1_dispatch'},
        {'start': '0x00000132', 'size': '0x04', 'kind': 'access', 'name': 'watch_0132_host_flags'},
        {'start': '0x0000002C', 'size': '0x02', 'kind': 'read',   'name': 'watch_002C_service_irq'},
        {'start': '0x0000D904', 'size': '0x02', 'kind': 'access', 'name': 'watch_D904_packet_status'},
    ]


def minimal_breakpoints() -> list[dict[str, str]]:
    return [
        {'address': '0x90006608', 'name': 'main_loop_90006608'},
        {'address': '0x90008824', 'name': 'packet_timer_done_90008824'},
        {'address': '0x9001CD4D', 'name': 'data_phase_helper_9001CD4D'},
    ]


def debug_breakpoints() -> list[dict[str, str]]:
    return minimal_breakpoints() + [
        {'address': '0x9001BBAC', 'name': 'host_gate_9001BBAC'},
        {'address': '0x900240EC', 'name': 'packet_service_gate_900240EC'},
        {'address': '0x900210C4', 'name': 'packet_service_900210C4'},
        {'address': '0x90024283', 'name': 'packet_fe_frame_copy_90024283'},
        {'address': '0x90021169', 'name': 'packet_tail_copy_90021169'},
        {'address': '0x900222C9', 'name': 'data_phase_dispatch_900222C9'},
        {'address': '0x900222FF', 'name': 'data_phase_ready_900222FF'},
        {'address': '0x90022301', 'name': 'data_phase_call_9001CD4D_90022301'},
        {'address': '0x900099EA', 'name': 'gdr_data_transfer_helper_900099EA'},
        {'address': '0x900099FF', 'name': 'gdr_data_in_source_read_900099FF'},
        {'address': '0x90009A02', 'name': 'gdr_df70_write_90009A02'},
        {'address': '0x90009A0E', 'name': 'gdr_df2a_submit_90009A0E'},
        {'address': '0x90017775', 'name': 'irq_status_handler_90017775'},
        {'address': '0x9001B285', 'name': 'main_scheduler_9001B285'},
        {'address': '0x9001B71B', 'name': 'packet_scheduler_9001B71B'},
    ]


def build_scsi_script(args: argparse.Namespace, parser: GDRScsiParser) -> list[dict[str, str]]:
    if args.scsi_script:
        raw=json.loads(Path(args.scsi_script).read_text(encoding='utf-8'))
        items=raw.get('commands', raw) if isinstance(raw, dict) else raw
        if not isinstance(items, list):
            raise SystemExit('[-] --scsi-script must contain a list or an object with commands')
        return [dict(x) for x in items]
    if args.command_json:
        # Compatibility path: old already-resolved persistent command JSON.
        raw=json.loads(Path(args.command_json).read_text(encoding='utf-8'))
        items=raw.get('commands', raw) if isinstance(raw, dict) else raw
        if not isinstance(items, list):
            raise SystemExit('[-] --command-json must contain a list or an object with commands')
        # Convert resolved commands back to a raw CDB script where possible.
        out=[]
        for x in items:
            ent={'tag': x.get('tag') or x.get('title'), 'cdb_hex': x.get('cdb_hex') or x.get('cdb')}
            if x.get('data_out_hex') or x.get('data_out'):
                ent['data_out_hex']=x.get('data_out_hex') or x.get('data_out')
            out.append(ent)
        return out
    items = parser.script_from_unlock_log()
    if not args.no_post_unlock:
        items = parser.append_post_unlock_script(items)
    if not args.no_post_read10:
        items = parser.append_read10_script(items, args.read10)
    if getattr(args, 'windows_probes', False):
        items = parser.append_windows_probe_script(items)
    elif getattr(args, 'standard_probes', False):
        items = parser.append_standard_probe_script(items)
    return items


def build_config(args: argparse.Namespace, commands: list[dict[str, str]], stem: str) -> dict[str, Any]:
    trace_csv = f'{stem}_trace.csv'
    debug = bool(args.debug_trace)
    cfg: dict[str, Any] = {
        'test_name': 'GDR-8050L v146 live TCP state bridge persistent session',
        'workspace': WORKSPACE,
        'target_profile': 'GDR-8050L',
        'custom_bin': FWBIN,
        'custom_base_addr': '0x90000000',
        'entry_point': '0x90000020',
        'max_cycles': int(str(args.max_cycles), 0),
        'header_skip_bytes': '0x0',
        'config_version': 'v100',
        'requires_emulator_version': 'V7-direct-config-trace-v100-gcc-version-sync',
        'initial_registers': {'SP': '0x003FF000'},
        'memory_aliases': [{'name': 'SRAM alias 0x80000000->0x00000000', 'from': '0x80000000', 'to': '0x00000000', 'size': '0x400000'}],
        'trace': {
            'enabled': True,
            'live_instruction_log': bool(args.debug_instructions),
            'out_csv': trace_csv,
            'watchpoints': debug_watchpoints() if debug else minimal_watchpoints(),
            'breakpoints': debug_breakpoints() if debug else minimal_breakpoints(),
            'gdr_hardware_model': {
                'enabled': True,
                'emit_events': debug,
                'timer': {'enabled': True, 'addr': '0x0000DC54', 'base': '0xF000', 'ticks_per_cycle': '0x2000'},
                'packet_status': {'enabled': True, 'source': 'v139 persistent session shared GDR D904/D905 model'},
            },
            'gdr_packet_session_model': {
                'enabled': True,
                'emit_events': True,
                'commands': commands,
                'packet_flags': '0x20',
                'main_loop_pc': '0x90006608',
                'timer_done_pc': '0x90008824',
                'stop_on_complete': True,
                'host_event_gate': {
                    'enabled': not bool(args.no_host_gate),
                    'force_016d_active': True,
                    'force_016d_value': '0x01',
                    'force_00de_pollable': True,
                    'force_00de_value': '0x01',
                    'force_08a8': str(args.host_gate_08a8),
                    'force_d950_value': '0x10',
                    'post_frame_d950_value': '0x40',
                    'clear_08a8_after_frame': not bool(args.no_clear_08a8_after_frame),
                    'clear_08a8_after_tail': not bool(args.no_clear_08a8_after_tail),
                    'clear_d950_after_tail': not bool(args.no_clear_d950_after_tail),
                    'd950_zero_after_post_frame_reads': str(args.d950_zero_after_post_frame_reads),
                    'irq_after_d108_data_phase': not bool(args.no_irq_after_d108_data_phase),
                    'complete_data_phase_on_d108': not bool(args.no_d108_hw_data_phase),
                    'seed_mem8': {
                        '0x000005B1': '0x20',
                        '0x000000DE': '0x01',
                        '0x0000016D': '0x01',
                        '0x000008A8': str(args.host_gate_08a8),
                        '0x000008A9': '0x00',
                        '0x0000DA18': '0x00',
                    },
                },
            },
        },
        'notes': [
            'v146: live TCP SPTI completion-surface bridge. Raw SCSI/MMC CDB script is resolved into persistent firmware session commands, a catalog, JSONL host-bridge responses, and TCP host-bridge responses.',
            'Supported proven CDBs: READ CAPACITY(10), MODE SENSE(10/6) page 3E, MODE SELECT(10/6), READ DVD STRUCTURE sidecar packets, READ(10), and selected host probes including GET CONFIGURATION and READ TOC.',
            'D108 hardware data-phase completion remains baseline behavior.',
        ],
    }
    if args.iso:
        cfg['media'] = {'type': 'iso9660/raw2048', 'iso_path': args.iso, 'sector_size': 2048, 'present': True, 'tray_state': 'closed', 'source': '--iso'}
    return cfg




def payload_bytes_for_command(c: dict[str, str]) -> bytes:
    direction=str(c.get('direction','')).lower()
    payload_hex=c.get('data_in_hex') if direction=='in' else c.get('data_out_hex') if direction=='out' else ''
    payload_bytes=[]
    if payload_hex:
        for tok in str(payload_hex).replace(',', ' ').split():
            try: payload_bytes.append(int(tok, 16) & 0xFF)
            except Exception: pass
    return bytes(payload_bytes)


def write_compact_transcript(path: Path, commands: list[dict[str, str]], *, xdvd_json: str, iso: str | None, source: str, response_dir: Path | None = None) -> None:
    import hashlib
    rows=[]
    if response_dir:
        response_dir.mkdir(parents=True, exist_ok=True)
    for i,c in enumerate(commands):
        direction=str(c.get('direction','')).lower()
        payload=payload_bytes_for_command(c)
        response_file=''
        if response_dir is not None and payload:
            response_file=f'{i:03d}_{str(c.get("tag") or f"cmd{i:02d}").replace("/","_").replace("\\","_")}.bin'
            (response_dir / response_file).write_bytes(payload)
        rows.append({
            'index': i,
            'tag': c.get('tag') or f'cmd{i:02d}',
            'direction': direction,
            'cdb_hex': c.get('cdb_hex',''),
            'payload_len': len(payload),
            'payload_sha256': hashlib.sha256(payload).hexdigest() if payload else '',
            'payload_preview_hex': ' '.join(f'{b:02X}' for b in payload[:32]),
            'payload_file': response_file,
        })
    doc={
        'format': 'gdr8050l_persistent_session_transcript_v2',
        'xdvd_json': xdvd_json,
        'iso': iso,
        'source': source,
        'commands_expected': len(commands),
        'responses_dir': str(response_dir) if response_dir else '',
        'commands': rows,
    }
    path.write_text(json.dumps(doc, indent=2), encoding='utf-8')




def _cdb_bytes_from_hex(text: str) -> bytes:
    out=[]
    for tok in str(text or '').replace(',', ' ').split():
        try:
            out.append(int(tok, 16) & 0xFF)
        except Exception:
            pass
    return bytes(out)


def describe_cdb(cdb_hex: str) -> dict[str, Any]:
    cdb = _cdb_bytes_from_hex(cdb_hex)
    if not cdb:
        return {'opcode_hex': '', 'opcode_name': 'EMPTY', 'cdb_len': 0}
    op = cdb[0]
    names = {
        0x00: 'TEST UNIT READY',
        0x03: 'REQUEST SENSE(6)',
        0x12: 'INQUIRY(6)',
        0x15: 'MODE SELECT(6)',
        0x1A: 'MODE SENSE(6)',
        0x1B: 'START STOP UNIT',
        0x1E: 'PREVENT/ALLOW MEDIUM REMOVAL',
        0x25: 'READ CAPACITY(10)',
        0x28: 'READ(10)',
        0x35: 'SYNCHRONIZE CACHE(10)',
        0x43: 'READ TOC/PMA/ATIP',
        0x46: 'GET CONFIGURATION',
        0x4A: 'GET EVENT STATUS NOTIFICATION',
        0x51: 'READ DISC INFORMATION',
        0x55: 'MODE SELECT(10)',
        0x5A: 'MODE SENSE(10)',
        0xAD: 'READ DVD STRUCTURE',
    }
    d: dict[str, Any] = {'opcode_hex': f'0x{op:02X}', 'opcode_name': names.get(op, f'OP_0x{op:02X}'), 'cdb_len': len(cdb)}
    if op == 0x28 and len(cdb) >= 10:
        d['lba'] = int.from_bytes(cdb[2:6], 'big')
        d['blocks'] = int.from_bytes(cdb[7:9], 'big')
    if op in (0x1A, 0x5A) and len(cdb) >= 3:
        d['mode_page'] = f'0x{(cdb[2] & 0x3F):02X}'
    if op == 0xAD and len(cdb) >= 12:
        d['dvd_structure_format'] = f'0x{cdb[7]:02X}'
        d['allocation_len'] = int.from_bytes(cdb[8:10], 'big')
    if op in (0x03,0x12,0x1A) and len(cdb) >= 5:
        d['allocation_len'] = cdb[4]
    if op in (0x43,0x46,0x4A,0x51,0x5A) and len(cdb) >= 9:
        d['allocation_len'] = int.from_bytes(cdb[7:9], 'big')
    return d


def write_response_catalog(path: Path, commands: list[dict[str, str]], *, xdvd_json: str, iso: str | None, transcript_path: Path, response_dir: Path) -> None:
    """Write an iSCSI/command-server friendly response catalog.

    This is not a live iSCSI target yet.  It is a deterministic map from raw CDBs
    to GOOD-status payloads/no-data completions proven by the persistent firmware
    session run.  DATA OUT commands are included as accepted host payloads so the
    unlock/auth transcript can be replayed exactly.
    """
    import hashlib
    entries=[]
    for i,c in enumerate(commands):
        direction=str(c.get('direction','')).lower()
        payload=payload_bytes_for_command(c)
        tag=str(c.get('tag') or f'cmd{i:02d}')
        safe_tag=tag.replace('/','_').replace('\\','_')
        payload_file=''
        if payload:
            payload_file=f'{i:03d}_{safe_tag}.bin'
        entry={
            'index': i,
            'tag': tag,
            'direction': direction,
            'cdb_hex': c.get('cdb_hex',''),
            'cdb': describe_cdb(c.get('cdb_hex','')),
            'scsi_status': 'GOOD',
            'sense_key': 'NO SENSE',
            'asc': '0x00',
            'ascq': '0x00',
            'payload_len': len(payload),
            'payload_sha256': hashlib.sha256(payload).hexdigest() if payload else '',
            'payload_file': payload_file,
            'payload_preview_hex': ' '.join(f'{b:02X}' for b in payload[:64]),
            'data_role': 'device_to_host' if direction == 'in' else 'host_to_device' if direction == 'out' else 'no_data',
        }
        entries.append(entry)
    doc={
        'format': 'gdr8050l_response_catalog_v1',
        'purpose': 'Seed catalog for a future command-server/iSCSI bridge; generated from an exact persistent MN103S firmware session.',
        'xdvd_json': xdvd_json,
        'iso': iso,
        'transcript': str(transcript_path),
        'responses_dir': str(response_dir),
        'commands_expected': len(commands),
        'entries': entries,
    }
    path.write_text(json.dumps(doc, indent=2), encoding='utf-8')


def write_host_request_jsonl(path: Path, script_items: list[dict[str, str]]) -> None:
    """Write the raw CDB/DATA OUT request stream consumed by gdr_host_jsonl_bridge.py."""
    rows=[]
    for i,item in enumerate(script_items):
        row={
            'index': i,
            'tag': item.get('tag') or item.get('title') or f'cmd{i:03d}',
            'cdb_hex': item.get('cdb_hex') or item.get('cdb') or '',
        }
        if item.get('data_out_hex') or item.get('data_out'):
            row['data_out_hex'] = item.get('data_out_hex') or item.get('data_out')
        rows.append(row)
    path.write_text('\n'.join(json.dumps(r, separators=(',',':')) for r in rows) + ('\n' if rows else ''), encoding='utf-8')


def run_jsonl_host_bridge(script_dir: Path, args: argparse.Namespace, host_requests: Path, host_responses: Path, host_response_dir: Path, host_verify: Path, catalog_path: Path) -> None:
    cmd=[
        sys.executable, 'gdr_host_jsonl_bridge.py',
        args.xdvd_json,
    ]
    if args.iso:
        cmd.append(args.iso)
    if args.log:
        cmd.extend(['--log', args.log])
    cmd.extend([
        '--script', str(host_requests),
        '--out', str(host_responses),
        '--write-response-dir', str(host_response_dir),
        '--catalog', str(catalog_path),
        '--verify-md', str(host_verify),
    ])
    run_checked(cmd, cwd=script_dir)


def run_tcp_host_bridge(script_dir: Path, args: argparse.Namespace, host_requests: Path, tcp_responses: Path, tcp_response_dir: Path, tcp_verify: Path, catalog_path: Path) -> None:
    cmd=[
        sys.executable, 'gdr_host_tcp_bridge.py',
        args.xdvd_json,
    ]
    if args.iso:
        cmd.append(args.iso)
    if args.log:
        cmd.extend(['--log', args.log])
    cmd.extend([
        '--script', str(host_requests),
        '--out', str(tcp_responses),
        '--write-response-dir', str(tcp_response_dir),
        '--catalog', str(catalog_path),
        '--verify-md', str(tcp_verify),
        '--host', str(args.tcp_host),
        '--port', str(args.tcp_port),
        '--timeout', str(args.tcp_timeout),
    ])
    run_checked(cmd, cwd=script_dir)




def live_state_smoke_requests() -> list[dict[str, str]]:
    """Requests used to prove the TCP server keeps state across one live client.

    This deliberately exercises bridge-generated CHECK CONDITION paths as well as
    catalog-backed GOOD responses.  It is separate from the all-GOOD command
    catalog replay used by the firmware-session verifier.
    """
    return [
        {'op': 'state', 'tag': 'live_state_00_initial_state'},
        {'tag': 'live_state_01_test_unit_ready_present', 'cdb_hex': '00 00 00 00 00 00'},
        {'tag': 'live_state_02_prevent_medium_removal_on', 'cdb_hex': '1E 00 00 00 01 00'},
        {'op': 'eject', 'tag': 'live_state_03_eject_denied_prevented'},
        {'tag': 'live_state_04_request_sense_after_denied_eject', 'cdb_hex': '03 00 00 00 12 00'},
        {'tag': 'live_state_05_prevent_medium_removal_off', 'cdb_hex': '1E 00 00 00 00 00'},
        {'tag': 'live_state_06_start_stop_eject', 'cdb_hex': '1B 00 00 00 02 00'},
        {'tag': 'live_state_07_test_unit_ready_no_media', 'cdb_hex': '00 00 00 00 00 00'},
        {'tag': 'live_state_08_request_sense_after_no_media', 'cdb_hex': '03 00 00 00 12 00'},
        {'tag': 'live_state_09_start_stop_load', 'cdb_hex': '1B 00 00 00 03 00'},
        {'tag': 'live_state_10_test_unit_ready_loaded_locked', 'cdb_hex': '00 00 00 00 00 00'},
        {'tag': 'live_state_11_read_capacity_locked_after_load', 'cdb_hex': '25 00 00 00 00 00 00 00 00 00'},
        {'op': 'reset_auth', 'tag': 'live_state_12_force_unlocked_for_server_seed', 'stage': 'unlocked'},
        {'tag': 'live_state_13_read_capacity_unlocked_after_reset_auth', 'cdb_hex': '25 00 00 00 00 00 00 00 00 00'},
    ]


def write_jsonl(path: Path, rows: list[dict[str, str]]) -> None:
    path.write_text('\n'.join(json.dumps(r, separators=(',',':')) for r in rows) + ('\n' if rows else ''), encoding='utf-8')


def run_live_tcp_state_smoke(script_dir: Path, args: argparse.Namespace, request_path: Path, response_path: Path, response_dir: Path, verify_path: Path, catalog_path: Path) -> None:
    """Start the TCP bridge in actual server mode and drive it with a client."""
    import tempfile, time
    ready = Path(tempfile.mktemp(prefix=f'{args.stem}_tcp_ready_', suffix='.json', dir=str(script_dir)))
    server_cmd=[
        sys.executable, 'gdr_host_tcp_bridge.py',
        args.xdvd_json,
    ]
    if args.iso:
        server_cmd.append(args.iso)
    if args.log:
        server_cmd.extend(['--log', args.log])
    server_cmd.extend(['--catalog', str(catalog_path), '--serve', '--once', '--host', str(args.tcp_host), '--port', '0', '--ready-file', str(ready)])
    srv=subprocess.Popen(server_cmd, cwd=str(script_dir), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
    try:
        deadline=time.time()+float(args.tcp_timeout)
        while time.time() < deadline and not ready.exists():
            if srv.poll() is not None:
                out=srv.stdout.read() if srv.stdout else ''
                raise SystemExit(f'live TCP server exited before ready file; rc={srv.returncode}\n{out}')
            time.sleep(0.05)
        if not ready.exists():
            raise SystemExit('live TCP server did not create ready file before timeout')
        info=json.loads(ready.read_text(encoding='utf-8'))
        client_cmd=[
            sys.executable, 'gdr_host_tcp_client.py',
            '--host', str(info['host']), '--port', str(info['port']), '--timeout', str(args.tcp_timeout),
            '--script', str(request_path), '--out', str(response_path),
            '--write-response-dir', str(response_dir), '--verify-md', str(verify_path), '--catalog', str(catalog_path), '--allow-check-condition',
        ]
        run_checked(client_cmd, cwd=script_dir)
        try:
            srv.wait(timeout=float(args.tcp_timeout))
        except subprocess.TimeoutExpired:
            srv.kill(); srv.wait(timeout=2)
            raise SystemExit('live TCP server did not exit after one client')
        if srv.returncode != 0:
            out=srv.stdout.read() if srv.stdout else ''
            raise SystemExit(f'live TCP server failed rc={srv.returncode}\n{out}')
    finally:
        try:
            if srv.poll() is None:
                srv.kill(); srv.wait(timeout=2)
        except Exception:
            pass
        try:
            ready.unlink()
        except Exception:
            pass


def write_live_state_verify(path: Path, response_path: Path) -> None:
    rows=[]
    for line in response_path.read_text(encoding='utf-8').splitlines():
        line=line.strip()
        if line:
            rows.append(json.loads(line))
    by_tag={r.get('tag'): r for r in rows}
    errors=[]
    def require(cond: bool, msg: str) -> None:
        if not cond:
            errors.append(msg)
    require(len(rows) == 14, f'expected 14 live-state responses, got {len(rows)}')
    require(sum(1 for r in rows if r.get('ok')) == 12, 'expected 12 GOOD/ok live-state responses')
    require(sum(1 for r in rows if r.get('scsi_status') == 'CHECK CONDITION') == 2, 'expected 2 CHECK CONDITION live-state responses')
    denied=by_tag.get('live_state_03_eject_denied_prevented', {})
    require(denied.get('ok') is False and denied.get('sense_key_hex') == '0x05' and denied.get('asc') == '0x53' and denied.get('ascq') == '0x02', 'prevented eject did not produce ILLEGAL REQUEST/53/02')
    denied_sense=by_tag.get('live_state_04_request_sense_after_denied_eject', {})
    require(str(denied_sense.get('data_hex','')).startswith('70 00 05'), 'REQUEST SENSE after prevented eject did not report key 05')
    require('53 02' in str(denied_sense.get('data_hex','')), 'REQUEST SENSE after prevented eject missing ASC/ASCQ 53/02')
    no_media=by_tag.get('live_state_07_test_unit_ready_no_media', {})
    require(no_media.get('ok') is False and no_media.get('sense_key_hex') == '0x02' and no_media.get('asc') == '0x3A', 'TUR with tray open did not produce NOT READY/3A/00')
    no_media_sense=by_tag.get('live_state_08_request_sense_after_no_media', {})
    require(str(no_media_sense.get('data_hex','')).startswith('70 00 02'), 'REQUEST SENSE after no-media did not report key 02')
    require('3A 00' in str(no_media_sense.get('data_hex','')), 'REQUEST SENSE after no-media missing ASC/ASCQ 3A/00')
    cap_locked=by_tag.get('live_state_11_read_capacity_locked_after_load', {})
    require(str(cap_locked.get('data_hex','')).replace(' ','').upper() == '00001B4F00000800', 'locked READ CAPACITY after load mismatch')
    cap_unlocked=by_tag.get('live_state_13_read_capacity_unlocked_after_reset_auth', {})
    require(str(cap_unlocked.get('data_hex','')).replace(' ','').upper() == '00345B5F00000800', 'unlocked READ CAPACITY after reset_auth mismatch')
    lines=[]
    lines.append('# GDR live TCP state smoke verification')
    lines.append('')
    lines.append(f'requests={len(rows)}')
    lines.append(f'ok={sum(1 for r in rows if r.get("ok"))}')
    lines.append(f'check_condition={sum(1 for r in rows if r.get("scsi_status") == "CHECK CONDITION")}')
    lines.append(f'exact_live_state_smoke={str(not errors).lower()}')
    lines.append('')
    lines.append('| index | tag | status | ok | sense | len | media | tray | auth |')
    lines.append('|---:|---|---|---|---|---:|---|---|---|')
    for r in rows:
        sense=f"{r.get('sense_key_hex','')}/{r.get('asc','')}/{r.get('ascq','')}"
        lines.append(f"| {r.get('index')} | `{r.get('tag','')}` | {r.get('scsi_status','')} | {str(bool(r.get('ok'))).lower()} | {sense} | {r.get('data_len',0)} | {r.get('media_present','')} | {r.get('tray_state','')} | {r.get('auth_stage_after','')} |")
    if errors:
        lines.append('')
        lines.append('## Errors')
        for e in errors:
            lines.append(f'- {e}')
    path.write_text('\n'.join(lines)+'\n', encoding='utf-8')
    if errors:
        raise SystemExit(2)



def media_swap_state_smoke_request_sets(args: argparse.Namespace) -> list[tuple[str, list[dict[str, str]]]]:
    """Requests used to prove one TCP server can eject, unmount, mount/select media,
    and preserve the selected media across separate client connections.

    This is the bridge behavior needed before a real iSCSI/virtual-device front-end:
    a controller can swap the backing XDVD/ISO while later SCSI clients see the new
    media state and locked/unlocked capacity responses.
    """
    mount_req = {
        'op': 'mount',
        'tag': 'media_swap_08_mount_selected_media_cross_client',
        'xdvd_json': str(args.xdvd_json),
        'iso': str(args.iso or ''),
    }
    if args.log:
        mount_req['log'] = str(args.log)
    return [
        ('client1', [
            {'op': 'state', 'tag': 'media_swap_00_initial_state'},
            {'tag': 'media_swap_01_prevent_medium_removal_on', 'cdb_hex': '1E 00 00 00 01 00'},
            {'op': 'unmount', 'tag': 'media_swap_02_unmount_denied_prevented'},
            {'tag': 'media_swap_03_request_sense_after_denied_unmount', 'cdb_hex': '03 00 00 00 12 00'},
            {'tag': 'media_swap_04_prevent_medium_removal_off', 'cdb_hex': '1E 00 00 00 00 00'},
            {'op': 'unmount', 'tag': 'media_swap_05_unmount_media'},
            {'op': 'state', 'tag': 'media_swap_06_state_after_unmount'},
        ]),
        ('client2', [
            {'tag': 'media_swap_07_test_unit_ready_no_media_cross_client', 'cdb_hex': '00 00 00 00 00 00'},
            {'tag': 'media_swap_07b_request_sense_after_no_media_cross_client', 'cdb_hex': '03 00 00 00 12 00'},
            mount_req,
            {'op': 'state', 'tag': 'media_swap_09_state_after_mount'},
            {'tag': 'media_swap_10_read_capacity_locked_after_mount', 'cdb_hex': '25 00 00 00 00 00 00 00 00 00'},
        ]),
        ('client3', [
            {'op': 'media_info', 'tag': 'media_swap_11_media_info_cross_client'},
            {'op': 'reset_auth', 'tag': 'media_swap_12_force_unlocked_for_server_seed', 'stage': 'unlocked'},
            {'tag': 'media_swap_13_read_capacity_unlocked_after_mount', 'cdb_hex': '25 00 00 00 00 00 00 00 00 00'},
            {'tag': 'media_swap_14_read10_lba32_after_mount', 'cdb_hex': '28 00 00 00 00 20 00 00 01 00'},
        ]),
    ]


def run_media_swap_tcp_state_smoke(script_dir: Path, args: argparse.Namespace, request_path: Path, response_path: Path, response_dir: Path, verify_path: Path, catalog_path: Path) -> None:
    """Start one TCP bridge server and drive it with multiple client connections."""
    import tempfile, time
    ready = Path(tempfile.mktemp(prefix=f'{args.stem}_tcp_media_swap_ready_', suffix='.json', dir=str(script_dir)))
    request_sets = media_swap_state_smoke_request_sets(args)
    response_dir.mkdir(parents=True, exist_ok=True)
    all_requests=[]
    server_cmd=[
        sys.executable, 'gdr_host_tcp_bridge.py',
        args.xdvd_json,
    ]
    if args.iso:
        server_cmd.append(args.iso)
    if args.log:
        server_cmd.extend(['--log', args.log])
    server_cmd.extend(['--catalog', str(catalog_path), '--serve', '--max-clients', str(len(request_sets)), '--host', str(args.tcp_host), '--port', '0', '--ready-file', str(ready)])
    srv=subprocess.Popen(server_cmd, cwd=str(script_dir), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
    try:
        deadline=time.time()+float(args.tcp_timeout)
        while time.time() < deadline and not ready.exists():
            if srv.poll() is not None:
                out=srv.stdout.read() if srv.stdout else ''
                raise SystemExit(f'media-swap TCP server exited before ready file; rc={srv.returncode}\n{out}')
            time.sleep(0.05)
        if not ready.exists():
            raise SystemExit('media-swap TCP server did not create ready file before timeout')
        info=json.loads(ready.read_text(encoding='utf-8'))
        all_rows=[]
        for client_index,(client_name,rows) in enumerate(request_sets, 1):
            for r in rows:
                rr=dict(r); rr['client']=client_name; all_requests.append(rr)
            client_req = request_path.with_name(f'{request_path.stem}_{client_name}{request_path.suffix}')
            client_out = response_path.with_name(f'{response_path.stem}_{client_name}{response_path.suffix}')
            client_dir = response_dir / client_name
            write_jsonl(client_req, rows)
            client_cmd=[
                sys.executable, 'gdr_host_tcp_client.py',
                '--host', str(info['host']), '--port', str(info['port']), '--timeout', str(args.tcp_timeout),
                '--script', str(client_req), '--out', str(client_out),
                '--write-response-dir', str(client_dir), '--catalog', str(catalog_path), '--allow-check-condition',
            ]
            run_checked(client_cmd, cwd=script_dir)
            for line in client_out.read_text(encoding='utf-8').splitlines():
                if line.strip():
                    row=json.loads(line)
                    row['client']=client_name
                    row['client_index']=client_index
                    all_rows.append(row)
        try:
            srv.wait(timeout=float(args.tcp_timeout))
        except subprocess.TimeoutExpired:
            srv.kill(); srv.wait(timeout=2)
            raise SystemExit('media-swap TCP server did not exit after all clients')
        if srv.returncode != 0:
            out=srv.stdout.read() if srv.stdout else ''
            raise SystemExit(f'media-swap TCP server failed rc={srv.returncode}\n{out}')
        write_jsonl(request_path, all_requests)
        response_path.write_text('\n'.join(json.dumps(r, separators=(',',':')) for r in all_rows) + ('\n' if all_rows else ''), encoding='utf-8')
        write_media_swap_state_verify(verify_path, response_path)
    finally:
        try:
            if srv.poll() is None:
                srv.kill(); srv.wait(timeout=2)
        except Exception:
            pass
        try:
            ready.unlink()
        except Exception:
            pass


def write_media_swap_state_verify(path: Path, response_path: Path) -> None:
    rows=[]
    for line in response_path.read_text(encoding='utf-8').splitlines():
        line=line.strip()
        if line:
            rows.append(json.loads(line))
    by_tag={r.get('tag'): r for r in rows}
    errors=[]
    def require(cond: bool, msg: str) -> None:
        if not cond:
            errors.append(msg)
    require(len(rows) == 16, f'expected 16 media-swap responses, got {len(rows)}')
    require(sum(1 for r in rows if r.get('ok')) == 14, 'expected 14 GOOD/ok media-swap responses')
    require(sum(1 for r in rows if r.get('scsi_status') == 'CHECK CONDITION') == 2, 'expected 2 CHECK CONDITION media-swap responses')
    denied=by_tag.get('media_swap_02_unmount_denied_prevented', {})
    require(denied.get('ok') is False and denied.get('sense_key_hex') == '0x05' and denied.get('asc') == '0x53' and denied.get('ascq') == '0x02', 'prevented unmount did not produce ILLEGAL REQUEST/53/02')
    denied_sense=by_tag.get('media_swap_03_request_sense_after_denied_unmount', {})
    require(str(denied_sense.get('data_hex','')).startswith('70 00 05'), 'REQUEST SENSE after prevented unmount did not report key 05')
    state_after_unmount=by_tag.get('media_swap_06_state_after_unmount', {})
    require(state_after_unmount.get('media_present') is False and state_after_unmount.get('tray_state') == 'open', 'unmount did not leave tray open/no media')
    no_media=by_tag.get('media_swap_07_test_unit_ready_no_media_cross_client', {})
    require(no_media.get('ok') is False and no_media.get('sense_key_hex') == '0x02' and no_media.get('asc') == '0x3A', 'cross-client TUR with no media did not produce NOT READY/3A/00')
    no_media_sense=by_tag.get('media_swap_07b_request_sense_after_no_media_cross_client', {})
    require(str(no_media_sense.get('data_hex','')).startswith('70 00 02'), 'REQUEST SENSE after no-media did not report key 02')
    mounted=by_tag.get('media_swap_08_mount_selected_media_cross_client', {})
    require(mounted.get('ok') is True and mounted.get('media_present') is True and mounted.get('tray_state') == 'closed', 'mount did not restore media/closed tray')
    require(bool(str(mounted.get('image_sha1',''))), 'mount response did not include image_sha1')
    state_after_mount=by_tag.get('media_swap_09_state_after_mount', {})
    require(state_after_mount.get('media_present') is True and state_after_mount.get('tray_state') == 'closed' and state_after_mount.get('auth_stage_after') == 'locked', 'state after mount not locked/closed/present')
    cap_locked=by_tag.get('media_swap_10_read_capacity_locked_after_mount', {})
    require(str(cap_locked.get('data_hex','')).replace(' ','').upper() == '00001B4F00000800', 'locked READ CAPACITY after mount mismatch')
    media_info=by_tag.get('media_swap_11_media_info_cross_client', {})
    require(bool(str(media_info.get('image_sha1',''))), 'media_info missing image_sha1')
    cap_unlocked=by_tag.get('media_swap_13_read_capacity_unlocked_after_mount', {})
    require(str(cap_unlocked.get('data_hex','')).replace(' ','').upper() == '00345B5F00000800', 'unlocked READ CAPACITY after mount mismatch')
    read10=by_tag.get('media_swap_14_read10_lba32_after_mount', {})
    require(read10.get('ok') is True and int(read10.get('data_len') or 0) == 2048, 'READ(10) after mount did not return one sector')
    clients=sorted(set(str(r.get('client','')) for r in rows))
    require(clients == ['client1','client2','client3'], f'expected client1/client2/client3 rows, got {clients}')
    lines=[]
    lines.append('# GDR media-swap TCP state smoke verification')
    lines.append('')
    lines.append(f'requests={len(rows)}')
    lines.append(f'ok={sum(1 for r in rows if r.get("ok"))}')
    lines.append(f'check_condition={sum(1 for r in rows if r.get("scsi_status") == "CHECK CONDITION")}')
    lines.append(f'clients={len(clients)}')
    lines.append(f'exact_media_swap_state_smoke={str(not errors).lower()}')
    lines.append('')
    lines.append('| client | index | tag | status | ok | sense | len | media | tray | prevent | auth | image_sha1 |')
    lines.append('|---|---:|---|---|---|---|---:|---|---|---|---|---|')
    for r in rows:
        sense=f"{r.get('sense_key_hex','')}/{r.get('asc','')}/{r.get('ascq','')}"
        lines.append(f"| {r.get('client','')} | {r.get('index')} | `{r.get('tag','')}` | {r.get('scsi_status','')} | {str(bool(r.get('ok'))).lower()} | {sense} | {r.get('data_len',0)} | {r.get('media_present','')} | {r.get('tray_state','')} | {r.get('prevent_removal','')} | {r.get('auth_stage_after','')} | {str(r.get('image_sha1',''))[:12]} |")
    if errors:
        lines.append('')
        lines.append('## Errors')
        for e in errors:
            lines.append(f'- {e}')
    path.write_text('\n'.join(lines)+'\n', encoding='utf-8')
    if errors:
        raise SystemExit(2)



def server_tooling_request_sets(args: argparse.Namespace) -> list[tuple[str, list[dict[str, str]]]]:
    """Requests used to prove the standalone server/client helper scripts work.

    Unlike the built-in TCP/media-swap smoke, this path starts gdr_host_server.py
    and drives it through gdr_host_cli.py across separate client processes.  It
    is the packaging bridge between the validated protocol model and an actual
    always-on user-mode server that future iSCSI/virtual-device front-ends can
    talk to.
    """
    mount_req = {
        'op': 'mount',
        'tag': 'server_tool_09_mount_selected_media',
        'xdvd_json': str(args.xdvd_json),
        'iso': str(args.iso or ''),
    }
    if args.log:
        mount_req['log'] = str(args.log)
    return [
        ('client1', [
            {'op': 'state', 'tag': 'server_tool_00_initial_state'},
            {'tag': 'server_tool_01_test_unit_ready_present', 'cdb_hex': '00 00 00 00 00 00'},
            {'tag': 'server_tool_02_prevent_medium_removal_on', 'cdb_hex': '1E 00 00 00 01 00'},
        ]),
        ('client2', [
            {'op': 'eject', 'tag': 'server_tool_03_eject_denied_prevented'},
            {'tag': 'server_tool_04_request_sense_after_denied_eject', 'cdb_hex': '03 00 00 00 12 00'},
            {'tag': 'server_tool_05_prevent_medium_removal_off', 'cdb_hex': '1E 00 00 00 00 00'},
        ]),
        ('client3', [
            {'op': 'unmount', 'tag': 'server_tool_06_unmount_media'},
            {'tag': 'server_tool_07_test_unit_ready_no_media', 'cdb_hex': '00 00 00 00 00 00'},
            {'tag': 'server_tool_08_request_sense_after_no_media', 'cdb_hex': '03 00 00 00 12 00'},
            mount_req,
            {'tag': 'server_tool_10_read_capacity_locked_after_mount', 'cdb_hex': '25 00 00 00 00 00 00 00 00 00'},
        ]),
        ('client4', [
            {'op': 'media_info', 'tag': 'server_tool_11_media_info_after_mount'},
            {'op': 'reset_auth', 'tag': 'server_tool_12_force_unlocked_for_server_seed', 'stage': 'unlocked'},
            {'tag': 'server_tool_13_read_capacity_unlocked', 'cdb_hex': '25 00 00 00 00 00 00 00 00 00'},
            {'tag': 'server_tool_14_read10_lba32_after_mount', 'cdb_hex': '28 00 00 00 00 20 00 00 01 00'},
        ]),
    ]


def run_server_tooling_smoke(script_dir: Path, args: argparse.Namespace, request_path: Path, response_path: Path, response_dir: Path, verify_path: Path, catalog_path: Path) -> None:
    """Start gdr_host_server.py and drive it with gdr_host_cli.py clients."""
    import tempfile, time
    ready = Path(tempfile.mktemp(prefix=f'{args.stem}_server_tool_ready_', suffix='.json', dir=str(script_dir)))
    request_sets = server_tooling_request_sets(args)
    response_dir.mkdir(parents=True, exist_ok=True)
    server_cmd=[
        sys.executable, 'gdr_host_server.py',
        args.xdvd_json,
    ]
    if args.iso:
        server_cmd.append(args.iso)
    if args.log:
        server_cmd.extend(['--log', args.log])
    server_cmd.extend(['--catalog', str(catalog_path), '--max-clients', str(len(request_sets)), '--host', str(args.tcp_host), '--port', '0', '--ready-file', str(ready)])
    srv=subprocess.Popen(server_cmd, cwd=str(script_dir), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
    all_requests=[]
    all_rows=[]
    try:
        deadline=time.time()+float(args.tcp_timeout)
        while time.time() < deadline and not ready.exists():
            if srv.poll() is not None:
                out=srv.stdout.read() if srv.stdout else ''
                raise SystemExit(f'server-tooling TCP server exited before ready file; rc={srv.returncode}\n{out}')
            time.sleep(0.05)
        if not ready.exists():
            raise SystemExit('server-tooling TCP server did not create ready file before timeout')
        for client_index,(client_name,rows) in enumerate(request_sets, 1):
            for r in rows:
                rr=dict(r); rr['client']=client_name; all_requests.append(rr)
            client_req = request_path.with_name(f'{request_path.stem}_{client_name}{request_path.suffix}')
            client_out = response_path.with_name(f'{response_path.stem}_{client_name}{response_path.suffix}')
            client_dir = response_dir / client_name
            write_jsonl(client_req, rows)
            client_cmd=[
                sys.executable, 'gdr_host_cli.py',
                '--ready-file', str(ready), '--timeout', str(args.tcp_timeout),
                '--script', str(client_req), '--out', str(client_out),
                '--write-response-dir', str(client_dir), '--catalog', str(catalog_path), '--allow-check-condition',
            ]
            run_checked(client_cmd, cwd=script_dir)
            for line in client_out.read_text(encoding='utf-8').splitlines():
                if line.strip():
                    row=json.loads(line)
                    row['client']=client_name
                    row['client_index']=client_index
                    all_rows.append(row)
        try:
            srv.wait(timeout=float(args.tcp_timeout))
        except subprocess.TimeoutExpired:
            srv.kill(); srv.wait(timeout=2)
            raise SystemExit('server-tooling TCP server did not exit after all clients')
        if srv.returncode != 0:
            out=srv.stdout.read() if srv.stdout else ''
            raise SystemExit(f'server-tooling TCP server failed rc={srv.returncode}\n{out}')
        write_jsonl(request_path, all_requests)
        response_path.write_text('\n'.join(json.dumps(r, separators=(',',':')) for r in all_rows) + ('\n' if all_rows else ''), encoding='utf-8')
        write_server_tooling_verify(verify_path, response_path)
    finally:
        try:
            if srv.poll() is None:
                srv.kill(); srv.wait(timeout=2)
        except Exception:
            pass
        try:
            ready.unlink()
        except Exception:
            pass


def write_server_tooling_verify(path: Path, response_path: Path) -> None:
    rows=[]
    for line in response_path.read_text(encoding='utf-8').splitlines():
        line=line.strip()
        if line:
            rows.append(json.loads(line))
    by_tag={r.get('tag'): r for r in rows}
    errors=[]
    def require(cond: bool, msg: str) -> None:
        if not cond:
            errors.append(msg)
    require(len(rows) == 15, f'expected 15 server-tooling responses, got {len(rows)}')
    require(sum(1 for r in rows if r.get('ok')) == 13, 'expected 13 GOOD/ok server-tooling responses')
    require(sum(1 for r in rows if r.get('scsi_status') == 'CHECK CONDITION') == 2, 'expected 2 CHECK CONDITION server-tooling responses')
    denied=by_tag.get('server_tool_03_eject_denied_prevented', {})
    require(denied.get('ok') is False and denied.get('sense_key_hex') == '0x05' and denied.get('asc') == '0x53' and denied.get('ascq') == '0x02', 'prevented eject did not produce ILLEGAL REQUEST/53/02')
    denied_sense=by_tag.get('server_tool_04_request_sense_after_denied_eject', {})
    require(str(denied_sense.get('data_hex','')).startswith('70 00 05') and '53 02' in str(denied_sense.get('data_hex','')), 'REQUEST SENSE after prevented eject mismatch')
    no_media=by_tag.get('server_tool_07_test_unit_ready_no_media', {})
    require(no_media.get('ok') is False and no_media.get('sense_key_hex') == '0x02' and no_media.get('asc') == '0x3A', 'TUR with no media did not produce NOT READY/3A/00')
    no_media_sense=by_tag.get('server_tool_08_request_sense_after_no_media', {})
    require(str(no_media_sense.get('data_hex','')).startswith('70 00 02') and '3A 00' in str(no_media_sense.get('data_hex','')), 'REQUEST SENSE after no-media mismatch')
    cap_locked=by_tag.get('server_tool_10_read_capacity_locked_after_mount', {})
    require(str(cap_locked.get('data_hex','')).replace(' ','').upper() == '00001B4F00000800', 'locked READ CAPACITY after mount mismatch')
    cap_unlocked=by_tag.get('server_tool_13_read_capacity_unlocked', {})
    require(str(cap_unlocked.get('data_hex','')).replace(' ','').upper() == '00345B5F00000800', 'unlocked READ CAPACITY mismatch')
    read10=by_tag.get('server_tool_14_read10_lba32_after_mount', {})
    require(read10.get('ok') is True and int(read10.get('data_len') or 0) == 2048, 'READ(10) after mount did not return one sector')
    clients=sorted(set(str(r.get('client','')) for r in rows))
    require(clients == ['client1','client2','client3','client4'], f'expected four client groups, got {clients}')
    lines=[]
    lines.append('# GDR standalone server/tooling TCP smoke verification')
    lines.append('')
    lines.append(f'requests={len(rows)}')
    lines.append(f'ok={sum(1 for r in rows if r.get("ok"))}')
    lines.append(f'check_condition={sum(1 for r in rows if r.get("scsi_status") == "CHECK CONDITION")}')
    lines.append(f'clients={len(clients)}')
    lines.append(f'exact_server_tooling_smoke={str(not errors).lower()}')
    lines.append('')
    lines.append('| client | index | tag | status | ok | sense | len | media | tray | prevent | auth | image_sha1 |')
    lines.append('|---|---:|---|---|---|---|---:|---|---|---|---|---|')
    for r in rows:
        sense=f"{r.get('sense_key_hex','')}/{r.get('asc','')}/{r.get('ascq','')}"
        lines.append(f"| {r.get('client','')} | {r.get('index')} | `{r.get('tag','')}` | {r.get('scsi_status','')} | {str(bool(r.get('ok'))).lower()} | {sense} | {r.get('data_len',0)} | {r.get('media_present','')} | {r.get('tray_state','')} | {r.get('prevent_removal','')} | {r.get('auth_stage_after','')} | {str(r.get('image_sha1',''))[:12]} |")
    if errors:
        lines.append('')
        lines.append('## Errors')
        for e in errors:
            lines.append(f'- {e}')
    path.write_text('\n'.join(lines)+'\n', encoding='utf-8')
    if errors:
        raise SystemExit(2)




def spti_completion_surface_requests(args: argparse.Namespace) -> list[dict[str, object]]:
    """SPTI/SCSI_PASS_THROUGH-shaped requests with host-buffer limits.

    The first group is the normal passthrough path.  The second group proves the
    client applies Windows-style DataTransferLength/SenseInfoLength buffer sizes
    on top of whatever the CDB allocation length permits.
    """
    return [
        {'op': 'state', 'tag': 'spti_00_initial_state'},
        {'tag': 'spti_01_test_unit_ready_present', 'Cdb': [0x00,0x00,0x00,0x00,0x00,0x00], 'DataIn': 'none', 'DataTransferLength': 0},
        {'tag': 'spti_02_inquiry_36', 'CdbHex': '12 00 00 00 24 00', 'DataIn': 'in', 'DataTransferLength': 36},
        {'tag': 'spti_03_read_capacity_locked', 'CdbHex': '25 00 00 00 00 00 00 00 00 00', 'DataIn': 'in', 'DataTransferLength': 8},
        {'op': 'reset_auth', 'tag': 'spti_04_force_unlocked_for_spti_seed', 'stage': 'unlocked'},
        {'tag': 'spti_05_read_capacity_unlocked', 'CdbHex': '25 00 00 00 00 00 00 00 00 00', 'DataIn': 'in', 'DataTransferLength': 8},
        {'tag': 'spti_06_read10_lba32_one_sector', 'CdbHex': '28 00 00 00 00 20 00 00 01 00', 'DataIn': 'in', 'DataTransferLength': 2048},
        {'tag': 'spti_07_inquiry_host_buffer_8', 'CdbHex': '12 00 00 00 24 00', 'DataIn': 'in', 'DataTransferLength': 8},
        {'tag': 'spti_08_inquiry_cdb_alloc_8_host_buffer_36', 'CdbHex': '12 00 00 00 08 00', 'DataIn': 'in', 'DataTransferLength': 36},
        {'tag': 'spti_09_read10_lba32_host_buffer_512', 'CdbHex': '28 00 00 00 00 20 00 00 01 00', 'DataIn': 'in', 'DataTransferLength': 512},
        {'tag': 'spti_10_read10_lba32_host_buffer_4096', 'CdbHex': '28 00 00 00 00 20 00 00 01 00', 'DataIn': 'in', 'DataTransferLength': 4096},
        {'tag': 'spti_11_prevent_medium_removal_on', 'CdbHex': '1E 00 00 00 01 00', 'DataIn': 'none', 'DataTransferLength': 0},
        {'op': 'eject', 'tag': 'spti_12_eject_denied_sense_buffer_8', 'SenseInfoLength': 8},
        {'tag': 'spti_13_request_sense_after_denied_eject_host_buffer_6', 'CdbHex': '03 00 00 00 12 00', 'DataIn': 'in', 'DataTransferLength': 6},
        {'tag': 'spti_14_prevent_medium_removal_off', 'CdbHex': '1E 00 00 00 00 00', 'DataIn': 'none', 'DataTransferLength': 0},
        {'tag': 'spti_15_start_stop_eject', 'CdbHex': '1B 00 00 00 02 00', 'DataIn': 'none', 'DataTransferLength': 0},
        {'tag': 'spti_16_test_unit_ready_no_media', 'Cdb': [0x00,0x00,0x00,0x00,0x00,0x00], 'DataIn': 'none', 'DataTransferLength': 0, 'SenseInfoLength': 18},
        {'tag': 'spti_17_request_sense_after_no_media', 'CdbHex': '03 00 00 00 12 00', 'DataIn': 'in', 'DataTransferLength': 18},
        {'tag': 'spti_18_start_stop_load_for_error_surface', 'CdbHex': '1B 00 00 00 03 00', 'DataIn': 'none', 'DataTransferLength': 0, 'TimeOutValue': 10},
        {'tag': 'spti_19_unsupported_opcode_autosense', 'CdbHex': 'FF 00 00 00 00 00', 'DataIn': 'none', 'DataTransferLength': 0, 'SenseInfoLength': 18, 'TimeOutValue': 10},
        {'tag': 'spti_20_request_sense_after_unsupported', 'CdbHex': '03 00 00 00 12 00', 'DataIn': 'in', 'DataTransferLength': 18, 'SenseInfoLength': 18, 'TimeOutValue': 10},
    ]


def run_spti_completion_surface_smoke(script_dir: Path, args: argparse.Namespace, request_path: Path, response_path: Path, response_dir: Path, verify_path: Path, catalog_path: Path) -> None:
    """Start standalone server and drive it with the SPTI-shaped client."""
    import tempfile, time
    ready = Path(tempfile.mktemp(prefix=f'{args.stem}_spti_ready_', suffix='.json', dir=str(script_dir)))
    requests = spti_completion_surface_requests(args)
    write_jsonl(request_path, requests)
    srv_cmd=[
        sys.executable, 'gdr_host_server.py',
        str(args.xdvd_json), str(args.iso or ''), '--log', str(args.log or ''),
        '--catalog', str(catalog_path), '--host', str(args.tcp_host), '--port', '0',
        '--max-clients', '1', '--ready-file', str(ready),
    ]
    srv=subprocess.Popen(srv_cmd, cwd=str(script_dir), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
    try:
        deadline=time.time()+float(args.tcp_timeout)
        while time.time() < deadline and not ready.exists() and srv.poll() is None:
            time.sleep(0.05)
        if not ready.exists():
            out=srv.stdout.read() if srv.stdout else ''
            raise SystemExit(f'SPTI TCP server did not create ready file before timeout\n{out}')
        info=json.loads(ready.read_text(encoding='utf-8'))
        client_cmd=[
            sys.executable, 'gdr_host_spti_client.py',
            '--host', str(info['host']), '--port', str(info['port']), '--timeout', str(args.tcp_timeout),
            '--script', str(request_path), '--out', str(response_path),
            '--write-response-dir', str(response_dir), '--verify-md', str(verify_path),
            '--allow-check-condition',
        ]
        run_checked(client_cmd, cwd=script_dir)
        try:
            srv.wait(timeout=float(args.tcp_timeout))
        except subprocess.TimeoutExpired:
            srv.kill(); srv.wait(timeout=2)
            raise SystemExit('SPTI TCP server did not exit after client')
        if srv.returncode != 0:
            out=srv.stdout.read() if srv.stdout else ''
            raise SystemExit(f'SPTI TCP server failed rc={srv.returncode}\n{out}')
        write_spti_completion_surface_verify(verify_path, response_path)
    finally:
        try:
            if srv.poll() is None:
                srv.kill(); srv.wait(timeout=2)
        except Exception:
            pass
        try:
            ready.unlink()
        except Exception:
            pass


def write_spti_completion_surface_verify(path: Path, response_path: Path) -> None:
    rows=[]
    for line in response_path.read_text(encoding='utf-8').splitlines():
        line=line.strip()
        if line:
            rows.append(json.loads(line))
    by_tag={r.get('tag'): r for r in rows}
    errors=[]
    def require(cond: bool, msg: str) -> None:
        if not cond:
            errors.append(msg)
    require(len(rows) == 21, f'expected 21 SPTI completion-surface responses, got {len(rows)}')
    require(sum(1 for r in rows if r.get('ok')) == 18, 'expected 18 GOOD/ok SPTI completion-surface responses')
    require(sum(1 for r in rows if r.get('ScsiStatusName') == 'CHECK CONDITION') == 3, 'expected 3 CHECK CONDITION SPTI completion-surface responses')
    cap_locked=by_tag.get('spti_03_read_capacity_locked', {})
    require(str(cap_locked.get('DataInHex','')).replace(' ','').upper().startswith('00001B4F00000800'), 'locked READ CAPACITY mismatch')
    cap_unlocked=by_tag.get('spti_05_read_capacity_unlocked', {})
    require(str(cap_unlocked.get('DataInHex','')).replace(' ','').upper().startswith('00345B5F00000800'), 'unlocked READ CAPACITY mismatch')
    read10_full=by_tag.get('spti_06_read10_lba32_one_sector', {})
    require(read10_full.get('ok') is True and int(read10_full.get('DataTransferLength') or 0) == 2048, 'SPTI READ(10) did not return one full sector')
    inq_small=by_tag.get('spti_07_inquiry_host_buffer_8', {})
    require(int(inq_small.get('DataTransferLength') or 0) == 8 and int(inq_small.get('DataTransferLengthRaw') or 0) == 36, 'INQUIRY host buffer clamp 36->8 failed')
    inq_cdb_small=by_tag.get('spti_08_inquiry_cdb_alloc_8_host_buffer_36', {})
    require(int(inq_cdb_small.get('DataTransferLength') or 0) == 8 and int(inq_cdb_small.get('DataTransferLengthRaw') or 0) == 8, 'INQUIRY CDB allocation clamp to 8 failed')
    read10_512=by_tag.get('spti_09_read10_lba32_host_buffer_512', {})
    require(int(read10_512.get('DataTransferLength') or 0) == 512 and int(read10_512.get('DataTransferLengthRaw') or 0) == 2048, 'READ(10) host buffer clamp 2048->512 failed')
    read10_4096=by_tag.get('spti_10_read10_lba32_host_buffer_4096', {})
    require(int(read10_4096.get('DataTransferLength') or 0) == 2048 and int(read10_4096.get('DataTransferLengthResidual') or 0) == 2048, 'READ(10) oversized host buffer residual failed')
    denied=by_tag.get('spti_12_eject_denied_sense_buffer_8', {})
    require(denied.get('ok') is False and denied.get('ScsiStatus') == 2, 'prevented eject did not produce CHECK CONDITION')
    require(int(denied.get('SenseInfoLength') or 0) == 8 and int(denied.get('SenseInfoLengthRaw') or 0) >= 18, 'CHECK CONDITION sense buffer clamp to 8 failed')
    denied_sense=by_tag.get('spti_13_request_sense_after_denied_eject_host_buffer_6', {})
    require(str(denied_sense.get('DataInHex','')).startswith('70 00 05') and int(denied_sense.get('DataTransferLength') or 0) == 6, 'REQUEST SENSE host buffer clamp to 6 failed')
    no_media=by_tag.get('spti_16_test_unit_ready_no_media', {})
    require(no_media.get('ok') is False and no_media.get('ScsiStatus') == 2, 'SPTI TUR no-media did not produce CHECK CONDITION')
    no_media_sense=by_tag.get('spti_17_request_sense_after_no_media', {})
    require(str(no_media_sense.get('DataInHex','')).startswith('70 00 02'), 'SPTI REQUEST SENSE after no-media did not report key 02')
    loaded=by_tag.get('spti_18_start_stop_load_for_error_surface', {})
    require(loaded.get('ok') is True and loaded.get('media_present') is True and loaded.get('tray_state') == 'closed', 'SPTI START STOP load did not restore media')
    unsupported=by_tag.get('spti_19_unsupported_opcode_autosense', {})
    require(unsupported.get('ok') is False and unsupported.get('ScsiStatus') == 2 and unsupported.get('SrbStatus') == 132 and unsupported.get('SenseInfoHex','').startswith('70 00 05'), 'unsupported opcode did not produce CHECK CONDITION with autosense/SRB status')
    require(unsupported.get('DeviceIoControlOk') is True and unsupported.get('Win32Error') == 0, 'unsupported opcode transport surface should be DeviceIoControl success with target CHECK CONDITION')
    unsup_sense=by_tag.get('spti_20_request_sense_after_unsupported', {})
    require(str(unsup_sense.get('DataInHex','')).startswith('70 00 05') and '20 00' in str(unsup_sense.get('DataInHex','')), 'REQUEST SENSE after unsupported opcode did not report ILLEGAL REQUEST/20/00')
    for rr in rows:
        if rr.get('ScsiStatusName') == 'CHECK CONDITION':
            require(rr.get('SrbStatus') == 132 and rr.get('SrbStatusName') == 'SRB_STATUS_ERROR_AUTOSENSE_VALID', f'CHECK CONDITION row {rr.get("tag")} missing autosense SRB status')
            require(rr.get('DeviceIoControlOk') is True and rr.get('Win32Error') == 0, f'CHECK CONDITION row {rr.get("tag")} missing transport-success surface')
        elif rr.get('ScsiStatusName') == 'GOOD':
            require(rr.get('SrbStatus') == 1 and rr.get('SrbStatusName') == 'SRB_STATUS_SUCCESS', f'GOOD row {rr.get("tag")} missing SRB success status')
    lines=[]
    lines.append('# GDR SPTI completion-surface smoke verification')
    lines.append('')
    lines.append(f'requests={len(rows)}')
    lines.append(f'ok={sum(1 for r in rows if r.get("ok"))}')
    lines.append(f'check_condition={sum(1 for r in rows if r.get("ScsiStatusName") == "CHECK CONDITION")}')
    lines.append(f'payload_responses={sum(1 for r in rows if int(r.get("DataTransferLength") or 0) > 0)}')
    lines.append(f'exact_spti_completion_surface_smoke={str(not errors).lower()}')
    lines.append('')
    lines.append('| index | tag | status | srb | ok | requested | returned | raw | residual | sense | bytes | auth | media | tray |')
    lines.append('|---:|---|---|---:|---|---:|---:|---:|---:|---:|---:|---|---|---|')
    for r in rows:
        lines.append(f"| {r.get('index')} | `{r.get('tag','')}` | {r.get('ScsiStatusName','')} | {r.get('SrbStatus','')} | {str(bool(r.get('ok'))).lower()} | {r.get('DataTransferLengthRequested',0)} | {r.get('DataTransferLength',0)} | {r.get('DataTransferLengthRaw',0)} | {r.get('DataTransferLengthResidual',0)} | {r.get('SenseInfoLength',0)}/{r.get('SenseInfoLengthRaw',0)} | {r.get('BytesReturned',0)} | {r.get('auth_stage_after','')} | {r.get('media_present','')} | {r.get('tray_state','')} |")
    if errors:
        lines.append('')
        lines.append('## Errors')
        for e in errors:
            lines.append(f'- {e}')
    path.write_text('\n'.join(lines)+'\n', encoding='utf-8')
    if errors:
        raise SystemExit(2)

def main() -> int:
    ap=argparse.ArgumentParser(description='Run v146 persistent GDR-8050L live TCP/media-swap state bridge')
    ap.add_argument('xdvd_json', help='XDVD sidecar path')
    ap.add_argument('iso', nargs='?', help='ISO/raw2048 path stored into media metadata and used for READ(10)')
    ap.add_argument('--log', help='Unlock log path; default is .log next to xdvd_json')
    ap.add_argument('--scsi-script', help='raw SCSI CDB script JSON; parser resolves expected data')
    ap.add_argument('--command-json', help='compatibility input: prior resolved persistent command JSON converted to parser script where possible')
    ap.add_argument('--write-scsi-script', help='write the raw CDB script used by this run')
    ap.add_argument('--no-post-unlock', action='store_true', help='do not append post-unlock sidecar/MMC command script entries')
    ap.add_argument('--no-post-read10', action='store_true', help='do not append media-backed READ(10) script entries')
    ap.add_argument('--standard-probes', action='store_true', help='append standard host probes after READ(10): TEST UNIT READY, INQUIRY, REQUEST SENSE')
    ap.add_argument('--windows-probes', action='store_true', help='append broader Windows/OS optical-drive probe set after READ(10)')
    ap.add_argument('--write-response-dir', default=None, help='write one binary response/payload file per command; default is <stem>_responses')
    ap.add_argument('--write-transcript', default=None, help='write compact command transcript JSON; default is <stem>_transcript.json')
    ap.add_argument('--write-catalog', default=None, help='write command-server/iSCSI seed response catalog JSON; default is <stem>_catalog.json')
    ap.add_argument('--no-write-catalog', action='store_true', help='skip writing the response catalog')
    ap.add_argument('--no-run-jsonl-bridge', action='store_true', help='skip the stateful JSONL host bridge replay/verification')
    ap.add_argument('--no-run-tcp-bridge', action='store_true', help='skip the loopback TCP JSONL host bridge replay/verification')
    ap.add_argument('--write-tcp-jsonl', default=None, help='write TCP bridge JSONL responses; default is <stem>_tcp_responses.jsonl')
    ap.add_argument('--write-tcp-response-dir', default=None, help='write TCP bridge payload files; default is <stem>_tcp_responses')
    ap.add_argument('--write-tcp-verify', default=None, help='write TCP bridge verification md; default is <stem>_tcp_bridge_verify.md')
    ap.add_argument('--tcp-host', default='127.0.0.1', help='loopback TCP bridge host')
    ap.add_argument('--tcp-port', default='0', help='loopback TCP bridge port; 0 selects an ephemeral port')
    ap.add_argument('--tcp-timeout', default='10', help='loopback TCP bridge timeout seconds')
    ap.add_argument('--no-run-live-state-smoke', action='store_true', help='skip the live server/client stateful TCP smoke test')
    ap.add_argument('--write-live-state-requests', default=None, help='write live TCP state smoke requests; default is <stem>_live_state_requests.jsonl')
    ap.add_argument('--write-live-state-jsonl', default=None, help='write live TCP state smoke responses; default is <stem>_live_state_responses.jsonl')
    ap.add_argument('--write-live-state-response-dir', default=None, help='write live TCP state smoke payload files; default is <stem>_live_state_responses')
    ap.add_argument('--write-live-state-verify', default=None, help='write live TCP state smoke verification md; default is <stem>_live_state_verify.md')
    ap.add_argument('--no-run-media-swap-state-smoke', action='store_true', help='skip the media-swap live TCP state smoke test')
    ap.add_argument('--write-media-swap-requests', default=None, help='write combined media-swap state smoke requests; default is <stem>_media_swap_requests.jsonl')
    ap.add_argument('--write-media-swap-jsonl', default=None, help='write combined media-swap state smoke responses; default is <stem>_media_swap_responses.jsonl')
    ap.add_argument('--write-media-swap-response-dir', default=None, help='write media-swap TCP payload files; default is <stem>_media_swap_responses')
    ap.add_argument('--write-media-swap-verify', default=None, help='write media-swap state smoke verification md; default is <stem>_media_swap_verify.md')
    ap.add_argument('--no-run-server-tooling-smoke', action='store_true', help='skip standalone gdr_host_server.py/gdr_host_cli.py smoke test')
    ap.add_argument('--write-server-tooling-requests', default=None, help='write standalone server-tooling smoke requests; default is <stem>_server_tooling_requests.jsonl')
    ap.add_argument('--write-server-tooling-jsonl', default=None, help='write standalone server-tooling smoke responses; default is <stem>_server_tooling_responses.jsonl')
    ap.add_argument('--write-server-tooling-response-dir', default=None, help='write standalone server-tooling payload files; default is <stem>_server_tooling_responses')
    ap.add_argument('--write-server-tooling-verify', default=None, help='write standalone server-tooling smoke verification md; default is <stem>_server_tooling_verify.md')
    ap.add_argument('--no-run-spti-completion-surface-smoke', action='store_true', help='skip SPTI completion-surface smoke test')
    ap.add_argument('--write-spti-completion-surface-requests', default=None, help='write SPTI completion-surface smoke requests; default is <stem>_spti_completion_surface_requests.jsonl')
    ap.add_argument('--write-spti-completion-surface-jsonl', default=None, help='write SPTI completion-surface smoke responses; default is <stem>_spti_completion_surface_responses.jsonl')
    ap.add_argument('--write-spti-completion-surface-response-dir', default=None, help='write SPTI completion-surface smoke payload files; default is <stem>_spti_completion_surface_responses')
    ap.add_argument('--write-spti-completion-surface-verify', default=None, help='write SPTI completion-surface verification md; default is <stem>_spti_completion_surface_verify.md')
    ap.add_argument('--write-host-requests', default=None, help='write JSONL host requests used by the stateful bridge; default is <stem>_host_requests.jsonl')
    ap.add_argument('--write-host-jsonl', default=None, help='write JSONL host responses; default is <stem>_host_responses.jsonl')
    ap.add_argument('--write-host-response-dir', default=None, help='write host bridge payload files; default is <stem>_host_responses')
    ap.add_argument('--write-host-verify', default=None, help='write host bridge verification md; default is <stem>_host_bridge_verify.md')
    ap.add_argument('--read10', action='append', default=[], metavar='LBA[:BLOCKS]', help='post-unlock READ(10) range to append; repeatable')
    ap.add_argument('--stem', default='gdr8050l_auth_v146_spti_completion_surface', help='output stem')
    ap.add_argument('--max-cycles', default='240000')
    ap.add_argument('--no-host-gate', action='store_true')
    ap.add_argument('--host-gate-08a8', default='0x04')
    ap.add_argument('--no-clear-08a8-after-frame', action='store_true')
    ap.add_argument('--no-clear-08a8-after-tail', action='store_true')
    ap.add_argument('--no-clear-d950-after-tail', action='store_true')
    ap.add_argument('--d950-zero-after-post-frame-reads', default='4')
    ap.add_argument('--no-irq-after-d108-data-phase', action='store_true')
    ap.add_argument('--no-d108-hw-data-phase', action='store_true')
    ap.add_argument('--debug-trace', action='store_true')
    ap.add_argument('--debug-instructions', action='store_true')
    ap.add_argument('--verbose-commands', action='store_true')
    args=ap.parse_args()

    script_dir=Path(__file__).resolve().parent
    xdvd=Path(args.xdvd_json)
    log_path=Path(args.log) if args.log else xdvd.with_suffix('.log')
    parser=GDRScsiParser(xdvd, args.iso, log_path if log_path.exists() else None)
    script_items=build_scsi_script(args, parser)
    if args.write_scsi_script:
        Path(args.write_scsi_script).write_text(json.dumps({'commands': script_items}, indent=2), encoding='utf-8')
    commands=parser.commands_from_script_items(script_items)

    print('[V146] Persistent live TCP/SPTI completion-surface bridge session.', flush=True)
    print(f'[V146] XDVD sidecar: {args.xdvd_json}', flush=True)
    if args.iso:
        print(f'[V146] ISO metadata: {args.iso}', flush=True)
    if args.scsi_script:
        src=f'SCSI script: {args.scsi_script}'
    elif args.command_json:
        src=f'compat command JSON via parser: {args.command_json}'
    else:
        suffix=' + parser-resolved post-unlock/READ(10) extensions'
        if args.windows_probes:
            suffix += ' + Windows/OS probe extension'
        elif args.standard_probes:
            suffix += ' + standard host probe extension'
        src=f'unlock log CDB script: {log_path}{suffix}'
    print(f'[V146] Command source: {src}', flush=True)
    print(f'[V146] Commands queued: {len(commands)}; d108_hw_data_phase={0 if args.no_d108_hw_data_phase else 1}; debug_trace={1 if args.debug_trace else 0}; windows_probes={1 if args.windows_probes else 0}', flush=True)
    transcript_path = Path(args.write_transcript) if args.write_transcript else (Path(__file__).resolve().parent / f'{args.stem}_transcript.json')
    response_dir = Path(args.write_response_dir) if args.write_response_dir else (Path(__file__).resolve().parent / f'{args.stem}_responses')
    write_compact_transcript(transcript_path, commands, xdvd_json=args.xdvd_json, iso=args.iso, source=src, response_dir=response_dir)
    print(f'[OK] wrote compact transcript: {transcript_path.name}', flush=True)
    print(f'[OK] wrote response payload directory: {response_dir.name}', flush=True)
    catalog_path = Path(args.write_catalog) if args.write_catalog else (Path(__file__).resolve().parent / f'{args.stem}_catalog.json')
    if not args.no_write_catalog:
        write_response_catalog(catalog_path, commands, xdvd_json=args.xdvd_json, iso=args.iso, transcript_path=transcript_path, response_dir=response_dir)
        print(f'[OK] wrote command response catalog: {catalog_path.name}', flush=True)
    if args.verbose_commands:
        for i,c in enumerate(commands,1):
            payload = c.get('data_in_hex') if c.get('direction')=='in' else c.get('data_out_hex','')
            plen = len(str(payload).split()) if payload else 0
            print(f"[V146] {i:02d} {c.get('tag')} {c.get('direction')} cdb={c.get('cdb_hex')} bytes={plen}", flush=True)

    if not args.no_run_jsonl_bridge:
        host_requests = Path(args.write_host_requests) if args.write_host_requests else (Path(__file__).resolve().parent / f'{args.stem}_host_requests.jsonl')
        host_responses = Path(args.write_host_jsonl) if args.write_host_jsonl else (Path(__file__).resolve().parent / f'{args.stem}_host_responses.jsonl')
        host_response_dir = Path(args.write_host_response_dir) if args.write_host_response_dir else (Path(__file__).resolve().parent / f'{args.stem}_host_responses')
        host_verify = Path(args.write_host_verify) if args.write_host_verify else (Path(__file__).resolve().parent / f'{args.stem}_host_bridge_verify.md')
        write_host_request_jsonl(host_requests, script_items)
        print(f'[OK] wrote JSONL host requests: {host_requests.name}', flush=True)
        run_jsonl_host_bridge(script_dir, args, host_requests, host_responses, host_response_dir, host_verify, catalog_path)
        print(f'[OK] wrote JSONL host responses: {host_responses.name}', flush=True)
        print(f'[OK] wrote JSONL host response directory: {host_response_dir.name}', flush=True)
        print(f'[OK] wrote JSONL host bridge verify: {host_verify.name}', flush=True)

    if not args.no_run_tcp_bridge:
        if args.no_run_jsonl_bridge:
            # Ensure the TCP bridge still has a raw request stream to consume.
            host_requests = Path(args.write_host_requests) if args.write_host_requests else (Path(__file__).resolve().parent / f'{args.stem}_host_requests.jsonl')
            write_host_request_jsonl(host_requests, script_items)
            print(f'[OK] wrote host requests for TCP bridge: {host_requests.name}', flush=True)
        tcp_responses = Path(args.write_tcp_jsonl) if args.write_tcp_jsonl else (Path(__file__).resolve().parent / f'{args.stem}_tcp_responses.jsonl')
        tcp_response_dir = Path(args.write_tcp_response_dir) if args.write_tcp_response_dir else (Path(__file__).resolve().parent / f'{args.stem}_tcp_responses')
        tcp_verify = Path(args.write_tcp_verify) if args.write_tcp_verify else (Path(__file__).resolve().parent / f'{args.stem}_tcp_bridge_verify.md')
        run_tcp_host_bridge(script_dir, args, host_requests, tcp_responses, tcp_response_dir, tcp_verify, catalog_path)
        print(f'[OK] wrote TCP host responses: {tcp_responses.name}', flush=True)
        print(f'[OK] wrote TCP host response directory: {tcp_response_dir.name}', flush=True)
        print(f'[OK] wrote TCP host bridge verify: {tcp_verify.name}', flush=True)

    if not args.no_run_live_state_smoke:
        live_requests = Path(args.write_live_state_requests) if args.write_live_state_requests else (Path(__file__).resolve().parent / f'{args.stem}_live_state_requests.jsonl')
        live_responses = Path(args.write_live_state_jsonl) if args.write_live_state_jsonl else (Path(__file__).resolve().parent / f'{args.stem}_live_state_responses.jsonl')
        live_response_dir = Path(args.write_live_state_response_dir) if args.write_live_state_response_dir else (Path(__file__).resolve().parent / f'{args.stem}_live_state_responses')
        live_verify = Path(args.write_live_state_verify) if args.write_live_state_verify else (Path(__file__).resolve().parent / f'{args.stem}_live_state_verify.md')
        write_jsonl(live_requests, live_state_smoke_requests())
        print(f'[OK] wrote live TCP state requests: {live_requests.name}', flush=True)
        run_live_tcp_state_smoke(script_dir, args, live_requests, live_responses, live_response_dir, live_verify, catalog_path)
        write_live_state_verify(live_verify, live_responses)
        print(f'[OK] wrote live TCP state responses: {live_responses.name}', flush=True)
        print(f'[OK] wrote live TCP state response directory: {live_response_dir.name}', flush=True)
        print(f'[OK] wrote live TCP state verify: {live_verify.name}', flush=True)

    if not args.no_run_media_swap_state_smoke:
        multi_requests = Path(args.write_media_swap_requests) if args.write_media_swap_requests else (Path(__file__).resolve().parent / f'{args.stem}_media_swap_requests.jsonl')
        multi_responses = Path(args.write_media_swap_jsonl) if args.write_media_swap_jsonl else (Path(__file__).resolve().parent / f'{args.stem}_media_swap_responses.jsonl')
        multi_response_dir = Path(args.write_media_swap_response_dir) if args.write_media_swap_response_dir else (Path(__file__).resolve().parent / f'{args.stem}_media_swap_responses')
        multi_verify = Path(args.write_media_swap_verify) if args.write_media_swap_verify else (Path(__file__).resolve().parent / f'{args.stem}_media_swap_verify.md')
        run_media_swap_tcp_state_smoke(script_dir, args, multi_requests, multi_responses, multi_response_dir, multi_verify, catalog_path)
        print(f'[OK] wrote media-swap TCP state requests: {multi_requests.name}', flush=True)
        print(f'[OK] wrote media-swap TCP state responses: {multi_responses.name}', flush=True)
        print(f'[OK] wrote media-swap TCP state response directory: {multi_response_dir.name}', flush=True)
        print(f'[OK] wrote media-swap TCP state verify: {multi_verify.name}', flush=True)

    if not args.no_run_server_tooling_smoke:
        server_requests = Path(args.write_server_tooling_requests) if args.write_server_tooling_requests else (Path(__file__).resolve().parent / f'{args.stem}_server_tooling_requests.jsonl')
        server_responses = Path(args.write_server_tooling_jsonl) if args.write_server_tooling_jsonl else (Path(__file__).resolve().parent / f'{args.stem}_server_tooling_responses.jsonl')
        server_response_dir = Path(args.write_server_tooling_response_dir) if args.write_server_tooling_response_dir else (Path(__file__).resolve().parent / f'{args.stem}_server_tooling_responses')
        server_verify = Path(args.write_server_tooling_verify) if args.write_server_tooling_verify else (Path(__file__).resolve().parent / f'{args.stem}_server_tooling_verify.md')
        run_server_tooling_smoke(script_dir, args, server_requests, server_responses, server_response_dir, server_verify, catalog_path)
        print(f'[OK] wrote standalone server-tooling requests: {server_requests.name}', flush=True)
        print(f'[OK] wrote standalone server-tooling responses: {server_responses.name}', flush=True)
        print(f'[OK] wrote standalone server-tooling response directory: {server_response_dir.name}', flush=True)
        print(f'[OK] wrote standalone server-tooling verify: {server_verify.name}', flush=True)

    if not args.no_run_spti_completion_surface_smoke:
        spti_requests = Path(args.write_spti_completion_surface_requests) if args.write_spti_completion_surface_requests else (Path(__file__).resolve().parent / f'{args.stem}_spti_completion_surface_requests.jsonl')
        spti_responses = Path(args.write_spti_completion_surface_jsonl) if args.write_spti_completion_surface_jsonl else (Path(__file__).resolve().parent / f'{args.stem}_spti_completion_surface_responses.jsonl')
        spti_response_dir = Path(args.write_spti_completion_surface_response_dir) if args.write_spti_completion_surface_response_dir else (Path(__file__).resolve().parent / f'{args.stem}_spti_completion_surface_responses')
        spti_verify = Path(args.write_spti_completion_surface_verify) if args.write_spti_completion_surface_verify else (Path(__file__).resolve().parent / f'{args.stem}_spti_completion_surface_verify.md')
        run_spti_completion_surface_smoke(script_dir, args, spti_requests, spti_responses, spti_response_dir, spti_verify, catalog_path)
        print(f'[OK] wrote SPTI completion-surface requests: {spti_requests.name}', flush=True)
        print(f'[OK] wrote SPTI completion-surface responses: {spti_responses.name}', flush=True)
        print(f'[OK] wrote SPTI completion-surface response directory: {spti_response_dir.name}', flush=True)
        print(f'[OK] wrote SPTI completion-surface verify: {spti_verify.name}', flush=True)

    stem=args.stem
    trace_csv=f'{stem}_trace.csv'
    config_json=script_dir / f'{stem}_config.json'
    output_txt=script_dir / f'{stem}.output.txt'
    verify_md=f'{stem}_verify.md'
    cfg=build_config(args, commands, stem)
    config_json.write_text(json.dumps(cfg, indent=2), encoding='utf-8')
    print(f'[OK] wrote persistent config: {config_json.name}', flush=True)
    run_checked([sys.executable, 'mn103s_emu.py', config_json.name], cwd=script_dir, stdout_file=output_txt)
    print(f'[OK] emulator output: {output_txt.name}', flush=True)
    res=subprocess.run([sys.executable, 'analyze_gdr_persistent_session.py', trace_csv, '--config', config_json.name, '--out', verify_md], cwd=str(script_dir))
    if res.returncode == 0:
        print('[OK] v146 live-tcp/SPTI completion-surface persistent session completed exactly.', flush=True)
    else:
        print(f'[WARN] v146 verifier returned {res.returncode}; session incomplete or mismatched, but output/trace were written.', flush=True)
    print(f'[NEXT] Send {stem}.output.txt, {trace_csv}, {config_json.name}, {verify_md}, {stem}_catalog.json, {stem}_host_responses.jsonl, {stem}_host_bridge_verify.md, {stem}_tcp_responses.jsonl, {stem}_tcp_bridge_verify.md, {stem}_live_state_responses.jsonl, {stem}_live_state_verify.md, {stem}_media_swap_responses.jsonl, {stem}_media_swap_verify.md, {stem}_server_tooling_responses.jsonl, {stem}_server_tooling_verify.md, {stem}_spti_completion_surface_responses.jsonl, and {stem}_spti_completion_surface_verify.md.', flush=True)
    return 0


if __name__ == '__main__':
    raise SystemExit(main())
