import asyncio,json,hashlib,base64,sys
from pathlib import Path
from playwright.async_api import async_playwright
from офлайн_Node_мост import ROOT,open_offline,BROWSER
OUT=ROOT/'доказательства/редакция-4.13.0';rows=[]
async def record(name,fn):
 try:
  value=await fn();ok=bool(value);rows.append({'проверка':name,'пройдено':ok,'наблюдение':value if not isinstance(value,bool) else None});print(name,ok,flush=True)
 except Exception as e:rows.append({'проверка':name,'пройдено':False,'ошибка':str(e)});print(name,'FAIL',str(e)[:250],flush=True)
async def wait_download(page,count):await page.wait_for_function('n=>window.__saved.length>=n',arg=count,timeout=30000)
async def blob(page,index=-1):return await page.evaluate('async i=>({name:__saved.at(i).name,text:await __saved.at(i).blob.text()})',index)
async def fill_form(page,selector):
 await page.eval_on_selector(selector,r'''f=>{for(const e of f.elements){if(e.disabled)continue;if(e.type==='checkbox'||e.type==='radio'){e.checked=true;}else if(e.tagName==='SELECT'){e.value=[...e.options].find(o=>o.value&&o.value!=='')?.value||e.options[0]?.value;}else if(e.type==='date'){e.value=e.name.includes('срок')?'2026-10-04':'2026-09-02';}else if(e.type==='datetime-local'){e.value='2026-09-02T12:00';}else if(e.type==='number'||e.type==='range'){e.value=e.min||'1';}else if(e.type==='url'){e.value='https://example.org/evidence';}else if(e.type==='email'){e.value='test@example.org';}else if(!['file','submit','button','reset','hidden'].includes(e.type)){e.value=e.pattern==='[a-f0-9]{64}'?'a'.repeat(64):'Искусственный ввод проверки интерфейса';}e.dispatchEvent(new Event('input',{bubbles:true}));e.dispatchEvent(new Event('change',{bubbles:true}));}f.dispatchEvent(new Event('submit',{bubbles:true,cancelable:true}));}''')
async def main():
 async with async_playwright() as p:
  b=await p.chromium.launch(executable_path=BROWSER,headless=True,args=['--no-sandbox'])
  page,errors,req=await open_offline(b,'переносимый-аудит.html',1280)
  await page.wait_for_function("document.querySelectorAll('#права-аудита article').length>0",timeout=15000)
  await page.locator('[data-action="пример-панели"]').click()
  await page.wait_for_function("document.getElementById('статус-панели').textContent.includes('Исходников: 6')",timeout=40000)
  async def panel():
   state=await page.evaluate("()=>({status:document.getElementById('статус-панели').textContent,windows:document.querySelectorAll('.продольные-окна article').length,text:document.getElementById('окна-панели').textContent})");print('PANEL_STATE',state['status'],state['windows'],flush=True);return 'Исходников: 6' in state['status'] and state['windows']==3 and 'НЕ ЗАВЕРШЕНО' in state['text']
  await record('Панель: шесть искусственных исходников, три окна, отсутствие полевого допуска',panel)
  await page.locator('[data-action="экспорт-панели"]').click();await wait_download(page,1);first=await blob(page)
  await page.locator('[data-action="экспорт-панели"]').click();await wait_download(page,2);second=await blob(page)
  async def same():return first['text']==second['text']
  await record('Панель: два экспорта одного состояния побайтово совпадают',same)
  await page.locator('[data-action="очистить-панель"]').click()
  await page.locator('#импорт-панели').set_input_files({'name':'panel.json','mimeType':'application/json','buffer':first['text'].encode()})
  await page.wait_for_function("document.getElementById('статус-панели').textContent.includes('Исходников:')||document.getElementById('статус-панели').textContent.includes('Импорт не завершён')",timeout=40000)
  async def restored():return 'Исходников: 6' in await page.locator('#статус-панели').inner_text()
  await record('Панель: очистка и восстановление с проверкой исходников',restored)
  await page.locator('[data-action="печать-панели"]').click();await wait_download(page,3);printed=await blob(page)
  async def printpanel():return printed['name'].endswith('.html') and '<script' not in printed['text'] and 'Исходные байты включены в JSON' in printed['text']
  await record('Панель: самостоятельный HTML без исполняемого кода',printpanel)
  (OUT/'пример-печатной-панели.html.txt').write_text(printed['text'])
  await page.locator('#импорт-серий').set_input_files({'name':'bad.json','mimeType':'application/json','buffer':b'{"a":1,"a":2}'})
  await page.wait_for_function("document.getElementById('статус-панели').textContent.includes('Исходников: 7')",timeout=20000)
  await page.locator('#источники-панели').evaluate("e=>e.closest('details').open=true")
  async def badkept():return 'ОТЛОЖЕНА' in await page.locator('#источники-панели').inner_text() and 'bad.json' in await page.locator('#источники-панели').inner_text()
  await record('Панель: неверный исходник остаётся в карантине',badkept)
  async def err():return len(errors)==0
  await record('Панель: без необработанных исключений',err)
  await page.close()
  # Complete case cycle, with captured original attachment bytes and round trip.
  page,errors,req=await open_offline(b,'пакет-обжалования.html',1280);await page.wait_for_function("document.getElementById('дело-код').textContent.length>0")
  await fill_form(page,'#форма-процессуального-комплекта')
  async def five():return await page.locator('#документы-дела option').count()==5
  await record('Дело: пять исходных черновиков',five)
  await page.locator('#импорт-приложений').set_input_files({'name':'искусственное-приложение.txt','mimeType':'text/plain','buffer':'Искусственный файл; не реальное доказательство.\n'.encode()})
  await page.wait_for_function("document.getElementById('приложения-дела').textContent.includes('искусственное-приложение')",timeout=15000)
  await page.locator('#редактор-документа [name="состояние"]').select_option('отправлен')
  await page.locator('#редактор-документа [name="основание_изменения"]').fill('Искусственная проверка изменения состояния; реальной отправки не было.')
  await page.locator('#редактор-документа button[type="submit"]').click()
  await page.wait_for_function("document.getElementById('статус-дела').textContent.includes('внесены в историю')")
  await page.locator('#история-документа').evaluate("e=>{const d=e.closest('details');if(d)d.open=true}")
  async def hist():return 'отправлен' in await page.locator('#история-документа').inner_text()
  await record('Дело: смена состояния добавлена в историю',hist)
  await page.locator('[data-action="сохранить-комплект"]').click();await wait_download(page,1);case1=await blob(page)
  await page.locator('[data-action="сохранить-комплект"]').click();await wait_download(page,2);case2=await blob(page)
  async def case_repeat():return case1['text']==case2['text']
  await record('Дело: повторный экспорт воспроизводим',case_repeat)
  c=json.loads(case1['text'])
  async def casebytes():return len(c['документы'])==5 and c['приложения'][0]['ша256']==hashlib.sha256(base64.b64decode(c['приложения'][0]['база64'])).hexdigest()
  await record('Дело: исходное приложение и SHA-256 совпали',casebytes)
  await page.locator('#импорт-дела').set_input_files({'name':'case.json','mimeType':'application/json','buffer':case1['text'].encode()})
  await page.wait_for_timeout(1200)
  async def case_import():return len(errors)==0 and await page.locator('#документы-дела option').count()==5 and 'ошиб' not in (await page.locator('#статус-дела').inner_text()).lower()
  await record('Дело: импорт сохранённой папки',case_import)
  await page.locator('[data-action="печать-дела"]').click();await wait_download(page,3);pr=await blob(page)
  async def caseprint():return '<script' not in pr['text'] and c['идентификатор'] in pr['text']
  await record('Дело: самостоятельное печатное представление',caseprint)
  (OUT/'пример-печатного-дела.html.txt').write_text(pr['text'])
  await page.close()
  # Registry, seven roles and both directions using the same graph builder.
  page,errors,req=await open_offline(b,'реестр-требований.html',1280);await page.wait_for_timeout(500)
  await fill_form(page,'#форма-требования');await page.wait_for_timeout(500)
  await page.locator('[data-action="экспорт-требований"]').click();await page.wait_for_timeout(500)
  async def roles():
   return await page.locator('#журнал-требований article').count()==1 and 'не сформирован' in await page.locator('#статус-требований').inner_text() and len(errors)==0
  await record('Реестр: запись принята; неподдерживаемая подпись не выдаёт ложный успех',roles)
  await page.close()
  # Existing public forms: test DOM response with unmistakably artificial inputs.
  inventory=[]
  from bs4 import BeautifulSoup
  exclude={'переносимый-аудит.html','пакет-обжалования.html','реестр-требований.html'}
  for path in sorted(ROOT.glob('*.html')):
   if path.name in exclude:continue
   soup=BeautifulSoup(path.read_text(),'html.parser');forms=[f['id'] for f in soup.find_all('form',id=True)]
   controls=bool(soup.select('[data-filter-life],#entity-search,[data-atlas-filter],[data-doc-filter]'))
   if not forms and not controls:continue
   page,errors,req=await open_offline(b,path.name,1280);await page.wait_for_timeout(450)
   for formid in forms:
    before=await page.locator('main').inner_text();outputs_before=await page.locator('textarea[readonly],output').evaluate_all("es=>es.map(e=>e.value??e.textContent)")
    try:await fill_form(page,'#'+formid);await page.wait_for_timeout(350);after=await page.locator('main').inner_text();inventory.append({'страница':path.name,'форма':formid,'ответ_DOM_изменился':before!=after,'вывод_изменился':outputs_before!=await page.locator('textarea[readonly],output').evaluate_all("es=>es.map(e=>e.value??e.textContent)"),'ошибки':list(errors)})
    except Exception as e:inventory.append({'страница':path.name,'форма':formid,'ошибка_стенда':str(e),'ошибки':list(errors)})
   for selector in ['[data-filter-life]','[data-doc-filter]']:
    for element in await page.locator(selector).all():
     try:await element.click(timeout=1500)
     except:pass
   await page.close()
  await b.close()
 report={'редакция':'4.13.0-wip','режим':'Изолированный DOM с явно тестовым JSON-мостом в настоящий Node Worker того же ядра. fetch, digest главного DOM, localStorage, URL и Blob заменены. Node Worker и его криптография настоящие; браузерные Worker, CSP, скачивание и отмена этим мостом не подтверждаются.','проверки':rows,'существующие_формы':inventory,'не_проверено':['нативное скачивание','реальное HTTPS и CSP','настоящий Web Crypto API','буфер обмена ОС','системная печать','все переходы клавиатуры во всех динамических состояниях'],'полный_браузерный_рубеж':'ОТКРЫТ'}
 (OUT/'интеграционные-инструменты-Node.json').write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n')
 print('FINISHED',len(rows),len(inventory),flush=True)
 return all(x['пройдено'] for x in rows) and not any(x.get('ошибки') or x.get('ошибка_стенда') for x in inventory)
if __name__=='__main__':raise SystemExit(0 if asyncio.run(main()) else 1)
