"""Независимая приёмка единой редакции: контракты, старые ссылки и команды.

Сеть, настоящее хранилище входа, Desktop и микрофон не используются.
"""
import ast
from collections import Counter
import hashlib
import html
from html.parser import HTMLParser
import importlib.util
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import tempfile
import unittest

ROOT = Path(__file__).resolve().parents[2]
BOOK = ROOT / 'docs/codex-cli-voice-textbook.html'


def load(name):
    """Загружает проверяемый генератор, не вызывая его main."""
    spec=importlib.util.spec_from_file_location(name,ROOT/'scripts/audio-textbook'/(name+'.py'))
    module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module)
    return module


def blocks(source):
    """Возвращает исходный текст, а не HTML-теги синтаксической подсветки."""
    return [(lang,html.unescape(re.sub('<[^>]+>','',value))) for lang,value in re.findall(r'<pre><code class="language-([^"]+)"[^>]*>(.*?)</code></pre>',source,re.S)]


class RevisionTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.source=BOOK.read_text()
        cls.inventory=json.loads((ROOT/'scripts/audio-textbook/legacy-links.json').read_text())
        cls.ids=set(re.findall(r'\bid="([^"]+)"',cls.source))

    def test_native_v3_selector(self):
        # Проверяем значение независимо от HTML ↔ собственного генератора.
        module=load('annotate-contracts')
        params=module.CONTRACTS['rpc']['sent']['params']
        self.assertEqual(params['version'],'v3')
        self.assertEqual(params['transport']['type'],'webrtc')
        source=(ROOT/'docs/audio-source-snapshots/codex-rs/tui/src/app_server_session/realtime.rs').read_text()
        self.assertIn('RealtimeConversationVersion::V3',source)
        replies=module.CONTRACTS['rpc']['received']
        self.assertEqual(replies[0],{'id':10,'result':{}})
        self.assertEqual(replies[1]['method'],'thread/realtime/sdp')

    def test_native_transport_source(self):
        base=ROOT/'docs/audio-source-snapshots/codex-rs'
        tui=(base/'tui/src/lib.rs').read_text()
        client=(base/'app-server-client/src/lib.rs').read_text()
        for marker in ['AppServerTarget::Embedded','AppServerClient::InProcess','RemoteAppServerEndpoint::UnixSocket']:
            self.assertIn(marker,tui)
        self.assertIn('InProcessAppServerClient',client)
        self.assertIn('mpsc::channel',client)
        module=load('annotate-contracts')
        self.assertNotIn('stdio://',module.CONTRACTS['rpc']['endpoint'])
        self.assertIn('stdio://',module.CONTRACTS['speech-rpc']['endpoint'])

    def test_bash_auth_argument(self):
        # Stub python только записывает argv, не выполняет вложенный клиент.
        script=ROOT/'scripts/voice-probes/chatgpt-transcribe.sh'
        with tempfile.TemporaryDirectory(prefix='auth путь ') as folder:
            root=Path(folder);stub=root/'python3'
            stub.write_text('#!/bin/bash\nprintf "%s\\n" "$@"\n')
            stub.chmod(0o755)
            env={**os.environ,'PATH':str(root)+':'+os.environ['PATH'],'CODEX_HOME':str(root/'wrong default')}
            audio=str(root/'запись.flac');auth=str(root/'выбранный профиль.json')
            for args in [[audio,'--auth-file',auth],['--auth-file',auth,audio]]:
                result=subprocess.run(['bash',str(script),*args],env=env,text=True,capture_output=True)
                self.assertEqual(result.returncode,0,result.stderr)
                self.assertEqual(result.stdout.splitlines(),['-',audio,auth])
            old=subprocess.run(['bash',str(script),audio],env=env,text=True,capture_output=True)
            self.assertEqual(old.stdout.splitlines(),['-',audio,str(root/'wrong default/auth.json')])
            for args in [[],['--auth-file'],[audio,audio],['--unknown',audio]]:
                bad=subprocess.run(['bash',str(script),*args],env=env,text=True,capture_output=True)
                self.assertEqual(bad.returncode,2)

    def test_summary_contract(self):
        source=(ROOT/'docs/tts-in-fork.md').read_text()
        body=re.search(r'JSON: (\{[^\n]+\})',source[source.index('audio_summary/synthesize'):])[1]
        self.assertIn(json.loads(body)['format'],{'podcast','executive_briefing','study_guide'})
        self.assertIn('422',source);self.assertIn('404',source)

    def test_early_final_not_full_completion(self):
        # Контрпример показывает границу прежнего критерия: sticky final
        # может случиться до EOF. Он не доказывает весь будущий ответ.
        file_done,assistant_done=False,False
        assistant_done=True
        self.assertFalse(file_done and assistant_done)
        file_done=True
        self.assertTrue(file_done and assistant_done)
        for relative in ['webrtc-voice-isolated/voice-console.py','webrtc-voice-proxy/voice_core.py']:
            source=(ROOT/'scripts'/relative).read_text()
            self.assertIn('не доказывает ответ на весь файл',source)
            self.assertNotIn('нет полного ответа до timeout',source)
        self.assertIn('одной реплик',self.source)
        self.assertIn('не доказывает полный media drain',self.source)

    def test_all_legacy_targets_and_schemes(self):
        self.assertEqual(len(self.inventory['schemes']),23)
        for filename,mapping in self.inventory['links'].items():
            for old,new in mapping.items():
                self.assertIn(new,self.ids,(filename,old,new))
        for row in self.inventory['schemes']:
            self.assertIn(row['target'],self.ids)
            self.assertIn(row['target']+'-wire',self.ids)
        for filename in ['codex-voice-proxy-textbook.html','audio-ui-entrypoints.html']:
            source=(ROOT/'docs'/filename).read_text()
            self.assertNotIn('<section',source)
            self.assertEqual(set(re.findall(r'<li id="([^"]+)"',source)),set(self.inventory['links'][filename]))
            self.assertIn('location.replace',source)

    def test_all_headings_in_menu_and_markers_are_real(self):
        nav=re.search(r'<nav class="chapters".*?</nav>',self.source,re.S)[0]
        body=re.search(r'<main.*?</main>',self.source,re.S)[0]
        headings=re.findall(r'<h[234][^>]*id="([^"]+)"',body)
        for id_ in headings:self.assertTrue('href="#'+id_+'"' in nav,id_)
        counts=Counter(re.findall(r'\bid="([^"]+)"',self.source))
        self.assertEqual([id_ for id_,count in counts.items() if count>1],[])
        markers=set(re.findall(r'<marker\b[^>]*id="([^"]+)"',self.source))
        for reference in re.findall(r'url\(#([^)]+)\)',body):self.assertIn(reference,markers)
        figure=re.search(r'<figure\b[^>]*id="overview-flow".*?</figure>',body,re.S)[0]
        local=re.search(r'<a href="([^"]+)"[^>]*>(?:(?!</a>).)*Ваш компьютер',figure,re.S)[1]
        self.assertEqual(local,'#native-machine')
        remote=re.search(r'<a href="([^"]+)"[^>]*>(?:(?!</a>).)*Голосовой сервис',figure,re.S)[1]
        self.assertEqual(remote,'#remote-service')
        ui=re.search(r'<figure\b[^>]*id="ui-routes".*?</figure>',body,re.S)[0]
        for rect in re.findall(r'<rect\b[^>]*>',ui):
            self.assertIn('fill=',rect)
        for path in re.findall(r'<path\b[^>]*>',ui):
            self.assertIn('stroke=',path)

    def test_audit_questions_cover_every_chapter_and_ui_section(self):
        module=load('revise-textbook')
        self.assertEqual(set(module.REVIEWS),set(module.ORDER))
        chapters=re.findall(r'data-reviewed-chapter="([^"]+)"',self.source)
        self.assertEqual(chapters,module.ORDER)
        self.assertEqual(len(re.findall(r'data-reviewed-ui="([^"]+)"',self.source)),12)
        for question,answer,target in module.REVIEWS.values():
            self.assertIn(target,self.ids)
            self.assertGreater(len(answer),70)
        # Без JavaScript ни одна глава не должна остаться скрытой.
        for opening in re.findall(r'<section\b[^>]*class="chapter"[^>]*>',self.source):
            self.assertNotRegex(opening,r'\bhidden(?:=|\s|>)')

    def test_all_blocks_have_language_and_parseable_payload(self):
        values=blocks(self.source)
        self.assertEqual(len(values),self.source.count('<pre>'))
        self.assertEqual(self.source.count('class="copy"'),len(values))
        for lang,value in values:
            if lang=='json':json.loads(value)
            if lang=='bash':
                syntax=subprocess.run(['bash','-n'],input=value,text=True,capture_output=True)
                self.assertEqual(syntax.returncode,0,syntax.stderr)
        js=[value for lang,value in values if 'startDictation' in value]
        self.assertTrue(js)
        self.assertTrue(all(lang=='javascript' for lang,value in values if 'startDictation' in value))

    def test_commands_choose_auth_without_conflicting_key(self):
        names=['chatgpt-transcribe.sh','chatgpt-dictation-file.py','http-tts.py','chatgpt-read-aloud.py','realtime-tts.py','appserver-tts.py','realtime-transcribe.py','voice-console.py','proxy-voice.py','run-proxy.sh']
        for lang,value in blocks(self.source):
            if lang!='bash':continue
            for command in value.replace('\\\n',' ').splitlines():
                # setup/cp и комментарий-объяснение не являются живым клиентом.
                if command.lstrip().startswith('#') or re.match(r'\s*(cp|pip|bash -n|python3 -m)',command):continue
                if not any(name in command for name in names):continue
                if '--api-key-file' in command:
                    self.assertNotIn('--auth-file',command,command)
                else:self.assertIn('--auth-file "$auth_file"',command,command)
                if 'realtime-tts.py' in command:
                    self.assertNotIn('$TTS_DEMO_CACHE/venv/bin/python',command)

    def test_snapshot_manifest_covers_every_source(self):
        directory=ROOT/'docs/audio-source-snapshots'
        manifest=json.loads((directory/'manifest.json').read_text())
        paths={item['path'] for item in manifest['files']}
        self.assertEqual(paths,{p.relative_to(directory).as_posix() for p in directory.rglob('*.rs')}|{'LICENSE'})
        for item in manifest['files']:
            self.assertEqual(hashlib.sha256((directory/item['path']).read_bytes()).hexdigest(),item['sha256'])

    def test_offline_highlight_preserves_exact_code(self):
        module=load('revise-textbook')
        for lang,text in [('bash','echo "<tag>" # русский комментарий'),('json','{"n":24,"s":"текст"}'),('python','return "<unsafe>"')]:
            value=module.highlight(text,lang)
            self.assertEqual(html.unescape(re.sub('<[^>]+>','',value)),text)

    def test_markdown_bash_examples_are_valid(self):
        # Проверка синтаксиса не запускает команды установки или live-клиент.
        names=['AUDIO-README.md','docs/chatgpt-audio-http.md','docs/voice-app-server.md','docs/tts-in-fork.md']
        names += ['scripts/'+folder+'/README.md' for folder in ['voice-probes','tts-demos','realtime-stt-isolated','remote-tts-isolated','webrtc-voice-isolated','webrtc-voice-proxy']]
        count=0
        for name in names:
            for code in re.findall(r'```bash\n(.*?)\n```',(ROOT/name).read_text(),re.S):
                result=subprocess.run(['bash','-n'],input=code,text=True,capture_output=True)
                self.assertEqual(result.returncode,0,name+': '+result.stderr)
                count+=1
        self.assertGreaterEqual(count,66)

    def test_revision_is_idempotent_in_temporary_copy(self):
        module=load('revise-textbook')
        names=['docs/codex-cli-voice-textbook.html','docs/codex-voice-proxy-textbook.html','docs/audio-ui-entrypoints.html','scripts/audio-textbook/legacy-links.json',
               'docs/chatgpt-audio-http.md','docs/voice-app-server.md','docs/tts-in-fork.md']
        names += ['scripts/'+folder+'/README.md' for folder in ['voice-probes','tts-demos','realtime-stt-isolated','remote-tts-isolated','webrtc-voice-isolated','webrtc-voice-proxy']]
        with tempfile.TemporaryDirectory(prefix='редакция с пробелами ') as folder:
            root=Path(folder)
            for name in names:
                (root/name).parent.mkdir(parents=True,exist_ok=True)
                shutil.copyfile(ROOT/name,root/name)
            module.ROOT=root
            module.DOCS=root/'docs'
            module.polish();module.polish_guides()
            first={name:hashlib.sha256((root/name).read_bytes()).hexdigest() for name in names}
            module.polish();module.polish_guides()
            second={name:hashlib.sha256((root/name).read_bytes()).hexdigest() for name in names}
            self.assertEqual(first,second)

    def test_own_demo_modules_contain_russian_explanations(self):
        for folder in ['voice-probes','tts-demos','realtime-stt-isolated','remote-tts-isolated','webrtc-voice-isolated','webrtc-voice-proxy']:
            for file in (ROOT/'scripts'/folder).glob('*.py'):
                if file.name.startswith('test_'):continue
                source=file.read_text();ast.parse(source)
                self.assertRegex(source,r'[А-Яа-я]',str(file))


if __name__=='__main__':
    unittest.main()
