"""Disposable integration server only. Usage: python http-test.py http://127.0.0.1:8765 path/to/test.sqlite"""
import sys, json, urllib.request, urllib.error, http.cookiejar, sqlite3, time, os, subprocess
BASE=sys.argv[1].rstrip('/')
DB=sys.argv[2]
passed=0
def check(value,label):
 global passed
 assert value,label
 passed+=1
 print('PASS',label)
class Client:
 def __init__(self):
  self.opener=urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
  self.csrf=self.call('me')[1]['data']['csrf']
 def call(self,action,body=None,csrf=True):
  headers={}
  if body is not None:
   headers={'Content-Type':'application/json'}
   if csrf:headers['X-CSRF-Token']=self.csrf
  req=urllib.request.Request(BASE+'/api.php?action='+action,data=None if body is None else json.dumps(body).encode(),headers=headers)
  try:
   with self.opener.open(req) as r:return r.status,json.load(r)
  except urllib.error.HTTPError as e:return e.code,json.load(e)
 def upload(self,pdf):
  boundary='MedPrepTestBoundary923723'
  fields={'meta':json.dumps({'title':'TEST BOOK','subject':'Test subject','author':'Test author','edition':'Test edition','academic_year':'Test year','chapter':'Test chapter'}),'total_pages':'2'}
  chunks=[]
  for k,v in fields.items():chunks.append(f'--{boundary}\r\nContent-Disposition: form-data; name="{k}"\r\n\r\n{v}\r\n'.encode())
  chunks.append(f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="test.pdf"\r\nContent-Type: application/pdf\r\n\r\n'.encode()+pdf+b'\r\n')
  chunks.append(f'--{boundary}--\r\n'.encode())
  req=urllib.request.Request(BASE+'/api.php?action=upload',data=b''.join(chunks),headers={'Content-Type':'multipart/form-data; boundary='+boundary,'X-CSRF-Token':self.csrf})
  with self.opener.open(req) as r:return json.load(r)['data']
admin=Client()
check(admin.call('register',{'name':'early','email':'early@example.test','password':'testing-pass-123'})[0]==403,'registration cannot precede initial admin setup')
check(admin.call('setup',{'name':'Test Admin','email':'admin@example.test','password':'testing-pass-123','setup_token':'wrong'})[0]==403,'setup rejects wrong token')
status,r=admin.call('setup',{'name':'Test Admin','email':'admin@example.test','password':'testing-pass-123','setup_token':'medprep-testing-token-not-for-production-123456'})
check(status==200 and r['data']['user']['role']=='admin','first admin setup succeeds')
check(admin.call('setup',{})[0]==409,'setup locks after installation')
student=Client()
check(student.call('register',{'name':'Test Student','email':'student@example.test','password':'testing-pass-123'})[0]==200,'student registration')
check(student.call('import',{'questions':[]})[0]==403,'student cannot import public questions')
check(student.call('logout',{},csrf=False)[0]==403,'missing CSRF rejected')
check(student.call('start',{'count':5,'minutes':5,'kind':'generated'})[0]==409,'empty bank cannot generate fake exam')
con=sqlite3.connect(DB) if DB!='mysql' else None
doc=student.upload(b'%PDF-1.4\n% test fixture only\n%%EOF')
did=doc['id']
check(admin.call('documents')[1]['data']==[],'one user cannot list another users books')
check(student.call('pages',{'document_id':did,'pages':[{'page':1,'lines':['Alpha is a test source passage with adequate content. This additional text makes the fixture long enough for the question-generation pipeline.'],'method':'text'}]})[0]==200,'page extraction persists')
check(student.call('finalize_document',{'document_id':did})[0]==400,'incomplete document cannot be finalized')
check(student.call('pages',{'document_id':did,'pages':[{'page':2,'lines':[],'method':'unreadable'}]})[0]==200,'unreadable pages recorded explicitly')
check(student.call('finalize_document',{'document_id':did})[1]['data']['unreadable_pages']==[2],'unreadable page numbers returned')
check(student.call('generate',{'document_id':did,'count':3})[0]==503,'missing AI config clearly reported, no simulated fallback')
check(student.call('pages',{'document_id':did,'pages':[]})[0]==409,'finalized sources cannot be silently rewritten')
other=Client();other.call('register',{'name':'Other Student','email':'other@example.test','password':'testing-pass-123'})
check(other.call('page&id='+did+'&page=1')[0]==404,'private source page isolation')
check(other.call('pdf&id='+did)[0]==403,'private original PDF isolation')
def q(i,year=1990):
 return {'question':f'Test original question {i}?','subject':'Test subject','chapter':'Test chapter','options':[{'text':f'Choice {j}','explanation':f'Reason for choice {j}'} for j in range(4)],'correct':0,'explanation':'Test rationale','exam_year':year,'exam_session':f'{year-1}-{year}','exam_name':'TEST FIXTURE','question_no':i,'answer_source':'Test answer document','source':{'url':'https://example.org/test','title':'TEST ONLY'}}
payload=[q(i) for i in range(1,5)]
check(admin.call('import',{'questions':payload})[1]['data']['added']==4,'PYQ import persists drafts')
check(student.call('start',{'count':1,'minutes':1,'kind':'pyq'})[0]==409,'unverified PYQs excluded from student exams')
rows=admin.call('admin')[1]['data']['questions']
check(admin.call('review',{'question_id':rows[0]['id'],'status':'verified','attested':False})[0]==400,'verification requires explicit attestation')
for row in rows:check(admin.call('review',{'question_id':row['id'],'status':'verified','attested':True,'note':'Test-only assertion'})[0]==200,'admin verification '+row['id'])
check(admin.call('import',{'questions':payload})[1]['data']['duplicates']==4,'repeat import is idempotent')
cov=student.call('coverage')[1]['data'];check(next(y for y in cov if y['year']==1990)['count']==4,'coverage counts only verified questions')
exam=student.call('start',{'count':3,'minutes':1,'kind':'pyq','penalty':0.25})[1]['data']
check(len({x['id'] for x in exam['questions']})==3,'exam has unique questions')
check(all('correct' not in x and 'source' not in x and all('explanation' not in o for o in x['options']) for x in exam['questions']),'active API never sends answer keys, explanations or sources')
check(student.call('start',{'count':1,'minutes':1,'kind':'pyq'})[1]['data']['id']==exam['id'],'starting twice resumes same active exam')
check(other.call('exam&id='+exam['id'])[0]==404,'exam ownership isolation')
check(student.call('chat',{'exam_id':exam['id'],'question_id':exam['questions'][0]['id'],'message':'why?'})[0]==403,'chat unavailable before submission')
for index,choice in [(0,'0'),(1,'1')]:
 check(student.call('answer',{'exam_id':exam['id'],'question_id':exam['questions'][index]['id'],'option_id':choice})[0]==200,'answer auto-save '+choice)
done=student.call('submit',{'exam_id':exam['id']})[1]['data'];check(done['result']['score']==0.75 and done['result']['unanswered']==1,'server scoring and unanswered handling')
check(student.call('submit',{'exam_id':exam['id']})[1]['data']['result']==done['result'],'double submit retains exactly same result')
check(student.call('answer',{'exam_id':exam['id'],'question_id':exam['questions'][1]['id'],'option_id':'0'})[1]['data']['result']==done['result'],'finished answers cannot change score')
check(student.call('chat',{'exam_id':exam['id'],'question_id':exam['questions'][0]['id'],'message':'Explain why'})[0]==503,'unconfigured chat returns actionable error')
exam2=student.call('start',{'count':1,'minutes':1,'kind':'pyq'})[1]['data'];check(exam2['questions'][0]['id'] not in [x['id'] for x in exam['questions']],'next exam prioritizes unseen questions')
if con:
 con.execute('UPDATE exams SET deadline=? WHERE id=?',(int(time.time())-1,exam2['id']));con.commit()
else:
 subprocess.run([os.environ['MEDPREP_PHP'],os.path.join(os.path.dirname(__file__),'expire-test.php'),exam2['id']],check=True)
expired=student.call('answer',{'exam_id':exam2['id'],'question_id':exam2['questions'][0]['id'],'option_id':'0'})[1]['data']
check(expired['status']=='finished' and expired['result']['unanswered']==1,'deadline rejects late answers and auto-submits')
check(student.call('start',{'count':1,'minutes':1,'kind':'pyq'})[0]==409,'exhausted bank does not silently repeat')
check(student.call('start',{'count':2,'minutes':1,'kind':'pyq','allow_repeat':True})[0]==200,'explicit repeat mode works')
check(student.call('report',{'question_id':exam['questions'][0]['id'],'message':'Test issue report'})[0]==200,'question issue reporting')
check(len(admin.call('admin')[1]['data']['reports'])==1,'admin receives issue report')
check(admin.call('export')[1]['data']['format']=='medprep-pyq-v1','question export format')
if con:con.close()
print('TOTAL',passed,'HTTP checks passed')
