#!/usr/bin/env python3
"""Browser installer integration against an explicitly isolated EMPTY MySQL database.
Requires DB_DATABASE ending _test and MULTISCAN_TEST_ALLOW_DATABASE_RESET=yes.
Creates/destroys only a temporary application checkout; refuses nonempty database.
"""
import os,pathlib,shutil,tempfile,subprocess,secrets,hashlib,socket,time,json,re,urllib.request,urllib.parse,urllib.error,http.cookiejar
APP=pathlib.Path(__file__).resolve().parents[2]
env=os.environ.copy()
if env.get('MULTISCAN_TEST_ALLOW_DATABASE_RESET')!='yes' or not env.get('DB_DATABASE','').endswith('_test'):raise SystemExit('Use an isolated empty database ending _test and explicit reset opt-in.')
PHP=env.get('PHP_BIN') or shutil.which('php');PORT=int(env.get('INSTALLER_TEST_PORT','8012'));BASE=f'http://127.0.0.1:{PORT}'
def raw_php(code,args=[]):return subprocess.run([PHP,'-r',code,*args],cwd=APP,env=env,check=True,capture_output=True,text=True).stdout
# Do not drop or reset arbitrary contents: caller must create a dedicated empty DB.
count=raw_php('$p=new PDO("mysql:host=".getenv("DB_HOST").";port=".getenv("DB_PORT").";dbname=".getenv("DB_DATABASE"),getenv("DB_USERNAME"),getenv("DB_PASSWORD"));echo $p->query("SHOW TABLES")->rowCount();')
if int(count)!=0:raise SystemExit('Refusing installer test: test database is not empty.')
with tempfile.TemporaryDirectory(prefix='multiscan-browser-test-') as temp:
 root=pathlib.Path(temp)
 shutil.copy2(APP/'composer.json',root/'composer.json')
 for folder in ['app','bootstrap','config','database','routes','public','resources']:shutil.copytree(APP/folder,root/folder,ignore=shutil.ignore_patterns('__pycache__'))
 for p in (root/'bootstrap/cache').glob('*.php'):p.unlink()
 (root/'vendor').symlink_to(APP/'vendor',target_is_directory=True)
 for folder in ['storage/app','storage/framework/sessions','storage/framework/views','storage/framework/cache/data','storage/logs']:(root/folder).mkdir(parents=True,exist_ok=True)
 key=secrets.token_hex(32);(root/'storage/app/installer-token.sha256').write_text(hashlib.sha256(key.encode()).hexdigest());(root/'INSTALLATION-KEY.txt').write_text(key)
 serverenv=env.copy()
 for name in list(serverenv):
  if name.startswith(('APP_','DB_','REDIS_','SESSION_','CACHE_','MAIL_','QUEUE_','LOG_')):serverenv.pop(name,None)
 logfile=open(root/'http.log','w');server=subprocess.Popen([PHP,'-S',f'127.0.0.1:{PORT}','-t',str(root/'public')],cwd=root,env=serverenv,stdout=logfile,stderr=logfile)
 opener=urllib.request.build_opener(urllib.request.ProxyHandler({}),urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
 def request(data=None):
  try:r=opener.open(urllib.request.Request(BASE+'/install.php',data=urllib.parse.urlencode(data).encode() if data is not None else None),timeout=90)
  except urllib.error.HTTPError as e:r=e
  return r.code,r.read().decode()
 try:
  for _ in range(100):
   try:status,body=request();break
   except OSError:time.sleep(.05)
  assert status==200 and 'Unlock installation' in body
  csrf=re.search(r'name="csrf" value="([^"]+)"',body).group(1)
  assert 'Unlock the installer first' in request({'csrf':csrf,'action':'install'})[1]
  assert 'setup key is incorrect' in request({'csrf':csrf,'action':'unlock','setup_key':'wrong'})[1]
  assert request({'csrf':'forged','action':'unlock','setup_key':key})[0]==419
  status,body=request({'csrf':csrf,'action':'unlock','setup_key':key});assert status==200 and 'Environment requirements' in body
  csrf=re.search(r'name="csrf" value="([^"]+)"',body).group(1)
  data={'csrf':csrf,'action':'install','site_name':'Installer Test','app_url':'https://scan.example.com','db_host':env['DB_HOST'],'db_port':env['DB_PORT'],'db_database':env['DB_DATABASE'],'db_username':env['DB_USERNAME'],'db_password':env.get('DB_PASSWORD',''),'redis_host':env.get('REDIS_HOST','127.0.0.1'),'redis_port':env.get('REDIS_PORT','6379'),'redis_password':env.get('REDIS_PASSWORD',''),'admin_name':'Owner','admin_email':'owner@example.com','admin_password':'OwnerPassword1234','admin_confirmation':'OwnerPassword1234','smtp_scheme':'smtp','smtp_port':'587'}
  raw_php('$p=new PDO("mysql:host=".getenv("DB_HOST").";port=".getenv("DB_PORT").";dbname=".getenv("DB_DATABASE"),getenv("DB_USERNAME"),getenv("DB_PASSWORD"));$p->exec("CREATE TABLE unrelated_existing_table (id INT)");')
  status,body=request(data);assert 'database is not empty' in body and not (root/'.env').exists()
  raw_php('$p=new PDO("mysql:host=".getenv("DB_HOST").";port=".getenv("DB_PORT").";dbname=".getenv("DB_DATABASE"),getenv("DB_USERNAME"),getenv("DB_PASSWORD"));$p->exec("DROP TABLE unrelated_existing_table");')
  status,body=request(data)
  if 'Application installed' not in body:raise AssertionError('Installer did not complete: '+re.sub('<[^>]+>',' ',body)[:3000]+' SERVER: '+(root/'http.log').read_text()[-2500:])
  assert (root/'.env').exists() and (root/'storage/app/installed.lock').exists()
  assert (root/'.env').stat().st_mode & 0o777==0o600
  assert not (root/'INSTALLATION-KEY.txt').exists() and not (root/'storage/app/installer-token.sha256').exists()
  assert request()[0]==404 and request(data)[0]==404
  valid=raw_php('$p=new PDO("mysql:host=".getenv("DB_HOST").";port=".getenv("DB_PORT").";dbname=".getenv("DB_DATABASE"),getenv("DB_USERNAME"),getenv("DB_PASSWORD"));$u=$p->query("SELECT * FROM users")->fetch(PDO::FETCH_ASSOC);echo ($u["is_admin"] && $u["email_verified_at"] && password_verify("OwnerPassword1234",$u["password"]))?"yes":"no";')
  assert valid=='yes'
  # Removal of .env alone cannot reopen a completed installer.
  saved=(root/'.env').read_bytes();(root/'.env').unlink();assert request()[0]==404;(root/'.env').write_bytes(saved)
  messages=['PASS nonempty database refused without overwriting tables or configuration','PASS unauthenticated and wrong-key setup requests rejected','PASS standalone installer opens before Laravel configuration','PASS real HTTP setup CSRF protection','PASS private setup-key authentication','PASS actual MySQL 8 migrations and plan/engine seeders through HTTP','PASS real Redis connection before installation','PASS hashed administrator password and verified admin account creation','PASS .env generated privately with mode 0600','PASS installation lock, GET/POST reinstallation refusal and key deletion','PASS removing .env alone cannot reopen installer']
  (APP/'test-results/installer-http.txt').write_text('\n'.join(messages)+'\n');print('\n'.join(messages))
 finally:
  server.terminate()
  try:server.wait(timeout=10)
  except subprocess.TimeoutExpired:server.kill();server.wait()
