#!/usr/bin/env python3
"""Destructive isolated-test integration runner. Requires existing MySQL/Redis.
Never run against production. Set MULTISCAN_TEST_ALLOW_DATABASE_RESET=yes.
"""
import os,pathlib,subprocess,socket,time,json,concurrent.futures,tempfile,shutil
APP=pathlib.Path(__file__).resolve().parents[2]
if os.environ.get('MULTISCAN_TEST_ALLOW_DATABASE_RESET')!='yes' or not os.environ.get('DB_DATABASE','').endswith('_test') or os.environ.get('DB_CONNECTION')!='mysql':
 raise SystemExit('Refusing reset: require explicit opt-in, DB_CONNECTION=mysql and an isolated database name ending _test')
if (APP/'bootstrap/cache/config.php').exists():
 raise SystemExit('Refusing cached configuration. Use an isolated checkout without cached production config.')
PHP=os.environ.get('PHP_BIN') or shutil.which('php')
if not PHP:raise SystemExit('PHP not found')
PORT=int(os.environ.get('MULTISCAN_TEST_HTTP_PORT','8011'));BASE=f'http://127.0.0.1:{PORT}'
LIVE=os.environ.get('MULTISCAN_LIVE_SCANNER')=='1'
env=os.environ.copy()
env.update(APP_ENV='local',APP_URL=BASE,APP_DEBUG='false',SESSION_DRIVER='file',SESSION_SECURE_COOKIE='false',CACHE_STORE='redis',QUEUE_CONNECTION='redis',MAIL_MAILER='array')
if not LIVE:env['SCANNER_SOCKET']='/nonexistent/multiscan-test.sock'
if LIVE and not env.get('QUARANTINE_PATH'):raise SystemExit('Live scanner acceptance requires explicit QUARANTINE_PATH')
env.setdefault('QUARANTINE_PATH',str(APP/'storage/app/integration-quarantine'))
if not env.get('APP_KEY'):raise SystemExit('Provide an isolated test APP_KEY')
# Redis must be dedicated: this test runs real queue consumers.
def php(*args,check=True,capture=True):return subprocess.run([PHP,*args],cwd=APP,env=env,check=check,capture_output=capture,text=True)
procs=[]
temporary=tempfile.TemporaryDirectory();tempdir=pathlib.Path(temporary.name)
try:
 php('artisan','migrate:fresh','--seed','--force')
 def consume(_):return php('tests/integration/quota-consume.php').stdout
 with concurrent.futures.ThreadPoolExecutor(max_workers=12) as pool:out=list(pool.map(consume,range(20)))
 assert out.count('accepted')==5 and out.count('limited')==15,out
 results=['PASS MySQL concurrent quota race: 20 processes, exactly 5 accepted and 15 limited']
 runid=php('tests/integration/queue-submit.php').stdout.strip();worker=php('artisan','queue:work','redis','--queue=scans,default','--stop-when-empty','--tries=1','--timeout=120');results.append(php('tests/integration/queue-check.php',runid).stdout.strip())
 # Start a real HTTP server in this network namespace. It has the real web CSRF middleware.
 server_log=open(tempdir/'http.log','w');procs.append(subprocess.Popen([PHP,'-S','127.0.0.1:'+str(PORT),'-t','public','public/index.php'],cwd=APP,env=env,stdout=server_log,stderr=server_log))
 import urllib.request,urllib.error,http.cookiejar,re,types
 class Response:
  def __init__(self,r):self.status_code=r.code;self.headers=r.headers;self.text=r.read().decode()
  def json(self):return json.loads(self.text)
 class Session:
  def __init__(self):self.opener=urllib.request.build_opener(urllib.request.ProxyHandler({}),urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
  def request(self,method,url,data=None,files=None,headers=None,timeout=10):
   headers=headers or {};body=None
   if files:
    boundary='MultiScanTestBoundary';chunks=[]
    for k,v in (data or {}).items():chunks.append(f'--{boundary}\r\nContent-Disposition: form-data; name="{k}"\r\n\r\n{v}\r\n'.encode())
    for k,(name,content) in files.items():chunks.append(f'--{boundary}\r\nContent-Disposition: form-data; name="{k}"; filename="{name}"\r\nContent-Type: application/octet-stream\r\n\r\n'.encode()+content+b'\r\n')
    body=b''.join(chunks)+f'--{boundary}--\r\n'.encode();headers['Content-Type']='multipart/form-data; boundary='+boundary
   elif data is not None:body=urllib.parse.urlencode(data).encode()
   try:r=self.opener.open(urllib.request.Request(url,data=body,headers=headers,method=method),timeout=timeout)
   except urllib.error.HTTPError as error:r=error
   return Response(r)
  def get(self,url,**kw):return self.request('GET',url,**kw)
  def post(self,url,**kw):return self.request('POST',url,**kw)
 requests=types.SimpleNamespace(Session=Session,RequestException=OSError)
 sess=requests.Session();sess.trust_env=False
 for _ in range(100):
  try:
   res=sess.get(BASE+'/',timeout=1)
   if res.status_code==200:break
  except requests.RequestException:pass
  time.sleep(.1)
 assert res.status_code==200,res.text[:500]
 token=re.search(r'name="csrf-token" content="([^"]+)"',res.text).group(1)
 bad=sess.post(BASE+'/scans',data={'type':'hash','hash':'a'*64},headers={'Accept':'application/json'});assert bad.status_code==419,bad.text[:500];results.append('PASS real HTTP request without CSRF token returns 419')
 res=sess.post(BASE+'/scans',data={'type':'file'},files={'file':('safe.txt',b'ordinary document')},headers={'Accept':'application/json','X-CSRF-TOKEN':token});assert res.status_code==201,res.text[:500];scan=res.json()
 stolen=requests.Session();stolen.trust_env=False;assert stolen.get(scan['url']).status_code==403;results.append('PASS fresh browser session cannot view another guest report')
 assert sess.get(scan['url']).status_code==200
 php('artisan','queue:work','redis','--queue=scans,default','--stop-when-empty','--tries=1','--timeout=120')
 data=sess.get(scan['url']+'/data').json()
 if LIVE:
  assert data['status']=='completed' and len(data['results'])==2 and all(r['verdict']=='undetected' for r in data['results']),data
  import base64
  eicar=base64.b64decode('WDVPIVAlQEFQWzRcUFpYNTQoUF4pN0NDKTd9JEVJQ0FSLVNUQU5EQVJELUFOVElWSVJVUy1URVNULUZJTEUhJEgrSCo=')
  result=sess.post(BASE+'/scans',data={'type':'file'},files={'file':('eicar.txt',eicar)},headers={'Accept':'application/json','X-CSRF-TOKEN':token});assert result.status_code==201,result.text
  php('artisan','queue:work','redis','--queue=scans,default','--stop-when-empty','--tries=1','--timeout=120')
  result=sess.get(result.json()['url']+'/data').json();assert result['status']=='completed' and len(result['results'])==2 and all(r['verdict']=='malicious' for r in result['results']),result
  results.append('PASS real HTTP → Redis → restricted socket → ClamAV/YARA pipeline for benign file and EICAR')
 else:
  assert data['status']=='failed' and data['results']==[],data
  results.append('PASS upload → Redis worker → scanner-unavailable errors → private report, with no fabricated verdicts; live socket mode still needs acceptance')
 assert sess.get(BASE+'/.env').status_code==404
 assert sess.get(BASE+'/scanner/daemon.py').status_code==404;results.append('PASS project .env and scanner source are outside the public document root')
 for header in ['X-Frame-Options','X-Content-Type-Options','Referrer-Policy']:assert header in res.headers,header
 results.append('PASS security response headers on real HTTP upload response')
 (APP/'test-results/integration.txt').write_text('\n'.join(results)+'\n');print('\n'.join(results),flush=True)
finally:
 for p in reversed(procs):
  p.terminate()
  try:p.wait(timeout=10)
  except subprocess.TimeoutExpired:p.kill();p.wait()
 temporary.cleanup()
