#!/usr/bin/env python3
"""Synthetic local release fixture. Imported by capture_driver.py; never accesses T3 data."""
import hashlib, io, json, socket, threading, time, tarfile, zipfile, select
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse, parse_qs

VERSION = '1.2.3'

def artifacts(node):
    executable = ('#!' + node + '\n' + '''console.log('1.2.3');
if(process.send){process.on('message', m=>{console.log('SYNTHETIC_IPC_RECEIVED '+JSON.stringify(m));process.send({fixture:'reply'});process.disconnect();});}
''').encode()
    # Deterministic bytes keep the archive large enough for visible progress.
    payload = b''.join(hashlib.sha256(str(i).encode()).digest() for i in range(8192))
    out = io.BytesIO()
    with tarfile.open(fileobj=out, mode='w:gz') as tar:
        for name, data, mode in [('t3', executable, 0o755),('fixture.bin',payload,0o644)]:
            info=tarfile.TarInfo('t3-1.2.3-linux-x64/'+name);info.size=len(data);info.mode=mode;info.mtime=0
            tar.addfile(info, io.BytesIO(data))
    archive=out.getvalue()
    z=io.BytesIO()
    with zipfile.ZipFile(z,'w') as zip:
        zip.writestr('t3-1.2.3-win32-x64/t3.exe',executable)
        zip.writestr('t3-1.2.3-win32-x64/fixture.bin',payload)
    sums=(hashlib.sha256(archive).hexdigest()+'  t3-1.2.3-linux-x64.tar.gz\n'+hashlib.sha256(z.getvalue()).hexdigest()+'  t3-1.2.3-win32-x64.zip\n').encode()
    return {'archive':archive,'zip':z.getvalue(),'sums':sums,'exe':executable}

class Fixture:
    def __init__(self, art):
        self.art=art; self.lock=threading.RLock();self.settled=threading.Condition(self.lock);self.stop=threading.Event();self.server=None
        owner=self
        class Handler(BaseHTTPRequestHandler):
            protocol_version='HTTP/1.1'
            def log_message(self,*args): pass
            def handle(self):
                try: super().handle()
                except (ConnectionResetError,BrokenPipeError):pass
            def do_GET(self): owner.handle(self)
        self.server=ThreadingHTTPServer(('127.0.0.1',0), Handler)
        self.server.daemon_threads=True
        self.thread=threading.Thread(target=self.server.serve_forever,daemon=True);self.thread.start()
        self.origin='http://127.0.0.1:'+str(self.server.server_port)
        self.connections=set()
    def reset(self, case, emit):
        with self.lock:
            self.case=case;self.emit=emit;self.counts={};self.requests=[];self.active=0;self.stop.clear();self.started=time.monotonic()
    def event(self, **event):
        with self.lock: self.emit('SERVER', event)
    def handle(self,h):
        with self.lock:
            self.active+=1; self.connections.add(h.connection)
            path=urlparse(h.path); q=parse_qs(path.query)
            key=('latest' if path.path.endswith('/latest') else 'api'+q.get('page',['1'])[0] if '/repos/' in path.path else 'sums' if path.path.endswith('/SHA256SUMS') else 'redirect' if path.path=='/archive-final' else 'archive')
            self.counts[key]=self.counts.get(key,0)+1; n=self.counts[key]
            seq=self.case.get('seq',[200]) if key==self.case.get('target','sums') else [200]
            value=seq[min(n-1,len(seq)-1)]
            if key=='redirect' and self.case.get('redirect_chunked'):value='chunked'
            if key==self.case.get('target') and n>len(seq): self.event(event='SENTINEL_OR_EXTRA_REQUEST',endpoint=key,attempt=n)
            if key=='latest': data=json.dumps(self.case.get('latest',{'tag_name':'v1.2.3'}),indent=2).encode()
            elif key.startswith('api'):
                page=int(key[3:]); channel=self.case.get('channel','nightly')
                tags=[{'tag_name':'v9.9.9-'+('preview' if channel=='nightly' else 'nightly')+'.20261110.'+str(i),'draft':False} for i in range(100)]
                if self.case.get('cap'): result=tags
                elif self.case.get('empty') and page>=2: result=[]
                elif page==1: result=tags
                else:
                    result=[{'tag_name':'v1.2.4-'+channel+'.20261110.1','draft':False}]
                    if self.case.get('draft_first'):result.insert(0,{'tag_name':'v9.9.9-'+channel+'.20261110.9','draft':True})
                data=json.dumps(result,indent=2).encode()
            elif key=='sums': data=self.art['sums'] if not self.case.get('invalid') else (b'bad checksum file\n' if self.case['invalid']=='missing' else b'0'*64+b'  t3-1.2.3-linux-x64.tar.gz\n'+b'0'*64+b'  t3-1.2.3-win32-x64.zip\n')
            else: data=self.art['zip'] if path.path.endswith('.zip') else self.art['archive']
            if self.case.get('refused') and key=='archive':value='refused_redirect'
            status=value if isinstance(value,int) else (200 if value in ('truncate','slow','chunked','hold','drop') else 302)
            item={'request_t':round(time.monotonic()-self.started,6),'event':'request','path':h.path,'endpoint':key,'attempt':n,'status':status if value!='drop' else 'DROP_PREHEADER','behavior':value,'accept':h.headers.get('Accept')}
            self.requests.append(item);self.event(**item)
        try:
            if value=='drop':
                h.connection.shutdown(socket.SHUT_RDWR); h.close_connection=True; return
            if value=='refused_redirect':
                h.send_response(302);h.send_header('Location',self.refused_location);h.send_header('Content-Length','0');h.send_header('Connection','close');h.end_headers();h.close_connection=True;return
            if value=='redirect':
                h.send_response(302);h.send_header('Location','/archive-final');h.send_header('Content-Length','19');h.end_headers();h.wfile.write(b'redirect length 019');h.wfile.flush();return
            h.send_response(status)
            h.send_header('Connection','close');h.close_connection=True
            if self.case.get('retry_after') and key==self.case.get('target') and status in (429,503):h.send_header('Retry-After',self.case['retry_after'])
            h.send_header('Content-Type','application/json' if key=='latest' or key.startswith('api') else 'application/octet-stream')
            if status!=200: data=('synthetic HTTP '+str(status)+'\n').encode()
            if value=='chunked': h.send_header('Transfer-Encoding','chunked')
            else: h.send_header('Content-Length',str(len(data)+100 if value=='truncate' else len(data)))
            h.end_headers()
            if value=='truncate':
                h.wfile.write(data[:max(1,len(data)//2)]);h.wfile.flush();h.connection.shutdown(socket.SHUT_RDWR);h.close_connection=True;return
            if value in ('slow','chunked','hold'):
                step=max(1,len(data)//16);sent=0
                for start in range(0,len(data),step):
                    if self.stop.is_set():
                        self.event(event='driver_stopped_stream',endpoint=key,sent=sent);return
                    if select.select([h.connection],[],[],0)[0] and h.connection.recv(1,socket.MSG_PEEK)==b'':
                        self.event(event='peer_eof_during_body',endpoint=key,sent=sent);return
                    chunk=data[start:start+step]
                    if value=='chunked':h.wfile.write(('%x\r\n'%len(chunk)).encode()+chunk+b'\r\n')
                    else:h.wfile.write(chunk)
                    h.wfile.flush();sent+=len(chunk)
                    self.event(event='body_chunk',endpoint=key,bytes=len(chunk),offset=start)
                    if self.stop.wait(0.12 if value!='hold' else 0.6):break
                if value=='chunked':h.wfile.write(b'0\r\n\r\n');h.wfile.flush()
            else: h.wfile.write(data);h.wfile.flush()
            self.event(event='body_complete',endpoint=key,bytes=len(data))
        except (BrokenPipeError,ConnectionResetError,OSError) as e:self.event(event='connection_closed_during_body',endpoint=key,error=type(e).__name__)
        finally:
            with self.lock:
                self.active-=1;self.connections.discard(h.connection);self.event(event='request_handler_end',endpoint=key);self.settled.notify_all()
    def settle(self):
        self.stop.set()
        with self.lock: conns=list(self.connections)
        for conn in conns:
            try:conn.shutdown(socket.SHUT_RDWR)
            except OSError:pass
        with self.settled:self.settled.wait_for(lambda:self.active==0,timeout=3)
    def close(self):
        self.settle();self.server.shutdown();self.server.server_close();self.thread.join()
