"""Run against a disposable copy only: python tests/integration.py BASE_URL SETUP_KEY.
Creates test users/listings and changes admin settings. Never run against production.
"""
import sys, requests, io, re
from PIL import Image
base=sys.argv[1].rstrip('/')
key=sys.argv[2]
passed=[]
class Client:
 def __init__(self):
  self.s=requests.Session();self.csrf=self.call('boot')['csrf']
 def call(self,a,b=None,expected=200,files=None,**params):
  url=base+'/api.php';params={'action':a,**params}
  if b is None:r=self.s.get(url,params=params)
  elif files:r=self.s.post(url,params=params,data=b,files=files,headers={'X-CSRF-Token':self.csrf})
  else:r=self.s.post(url,params=params,json=b,headers={'X-CSRF-Token':self.csrf})
  assert r.status_code==expected,(a,r.status_code,r.text[:300]);return r.json()
 def check(self,label):passed.append(label);print('PASS:',label)
admin=Client();admin.call('install',dict(key=key,name='Market Admin',email='owner@example.test',password='Owner-test-password-2026',site_url='https://market.example.test'))
admin.check('Secure owner setup and SQLite initialization')
assert admin.call('list')['total']==8
admin.call('install',dict(key=key),expected=409);admin.check('Installer locks after first administrator')
anon=Client();anon.call('admin',expected=401)
r=anon.s.post(base+'/api.php?action=admin_settings',json={'show_demo':False});assert r.status_code==403
anon.check('Anonymous admin access and missing CSRF are blocked')
seller=Client();seller.call('register',dict(name='Test Seller',email='seller@example.test',password='Seller-test-password',phone='08012345678'))
seller.call('admin',expected=403);seller.check('Seller role cannot access administrator endpoints')
buf=io.BytesIO();Image.new('RGB',(800,600),'white').save(buf,format='JPEG');raw=buf.getvalue()
fields=dict(title='Real test smartphone',description='A real test listing, excellent condition and carefully inspected.',price=75000,category='Phones',location='Abuja',condition='Used')
a=seller.call('save_ad',fields,files=[('photos[]',('test.jpg',raw,'image/jpeg'))]);aid=a['id'];assert a['status']=='pending';assert anon.call('list')['total']==8
anon.call('detail',id=aid,expected=404);seller.check('New real ads await moderation and are not exposed publicly')
d=seller.call('detail',id=aid);im=Image.open(io.BytesIO(anon.s.get(base+'/'+d['images'][0]).content));assert min(im.getpixel((im.width-15,im.height-15)))<240
seller.check('Uploaded photo is re-encoded and watermark is burned into pixels')
admin.call('admin_ad',dict(id=aid,status='active'));assert anon.call('list')['total']==9
admin.check('Admin approval publishes real ad beside all demo ads')
buyer=Client();buyer.call('register',dict(name='Test Buyer',email='buyer@example.test',password='Buyer-test-password',phone='08087654321'))
buyer.call('favorite',dict(id=1));buyer.call('favorite',dict(id=aid));assert len(buyer.call('favorites'))==2
admin.call('admin_settings',dict(show_demo=False));assert anon.call('list')['total']==1;assert buyer.call('list',saved=1)['total']==1;anon.call('detail',id=1,expected=404)
admin.check('Demo OFF hides demo in browse, favorites and direct URLs; real ad stays live')
admin.call('admin_settings',dict(show_demo=True));assert anon.call('list')['total']==9
admin.check('Demo ON restores samples without deleting real data')
assert anon.call('list',q='Real test',location='Abuja',max='80000')['total']==1
assert anon.call('list',q='Real test',max='10000')['total']==0
anon.check('Search, location and numeric price filters')
buyer.call('save_ad',dict(**fields,id=aid),expected=403)
buyer.call('delete_ad',dict(id=aid),expected=403);buyer.check('Ownership enforced for edits and deletion')
buyer.call('message',dict(ad_id=aid,body='Hello, is it still available?'));inbox=seller.call('inbox');assert len(inbox)==1
seller.call('message',dict(ad_id=aid,recipient_id=inbox[0]['sender_id'],body='Yes, it is available.'));assert len(buyer.call('inbox'))==2
buyer.check('Private buyer-seller conversation and seller reply')
buyer.call('message',dict(ad_id=1,body='Demo contact'),expected=400)
buyer.call('report',dict(id=aid,reason='Test moderation report'));data=admin.call('admin');assert data['stats']['reports']==1
admin.call('admin_report',dict(id=data['reports'][0]['id']));assert admin.call('admin')['stats']['reports']==0
admin.check('Listing reports and admin resolution')
seller.call('save_ad',fields,files=[('photos[]',('bad.php',b'<?php echo 1;', 'image/jpeg'))],expected=400)
seller.check('Disguised executable upload rejected')
admin.call('admin_settings',dict(moderation=False));r=seller.call('save_ad',fields,files=[('photos[]',('test.jpg',raw,'image/jpeg'))]);assert r['status']=='active'
seller.check('Moderation OFF publishes future real submissions immediately')
seller.call('ad_status',dict(id=r['id'],status='sold'));anon.call('detail',id=r['id'],expected=404)
seller.check('Sold ads removed from public listings')
profile=buyer.call('boot')['user'];admin.call('admin_user',dict(id=profile['id'],banned=True));buyer.call('mine',expected=401)
admin.check('Suspended users lose access immediately')
g=anon.s.get(base+'/google-auth.php',allow_redirects=False);assert g.status_code==302;assert g.headers['Location'].startswith('https://accounts.google.com/');assert 'code_challenge=' in g.headers['Location'];assert 'client_secret' not in g.headers['Location']
x=anon.s.get(base+'/google-auth.php?code=invalid&state=invalid',allow_redirects=False);assert x.status_code==302;assert 'could not be verified' in anon.call('boot')['flash']
anon.check('Google authorization redirect, PKCE and invalid-state rejection')
assert 'GOCSPX' not in requests.get(base+'/assets/app.js').text
anon.check('Google client secret absent from browser bundle')
# Return fixtures to useful browser-review state.
admin.call('admin_settings',dict(show_demo=True,moderation=True));seller.call('delete_ad',dict(id=r['id']))
print(f'\n{len(passed)} integration checks passed. Google live exchange requires a registered HTTPS domain.')
