"""HTTP regression tests. Run only against a disposable installation."""
import json,urllib.request,urllib.error,http.cookiejar,uuid,concurrent.futures
BASE='http://127.0.0.1:33318/manhour/'
checks=0
jar=http.cookiejar.CookieJar();opener=urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
def call(route,body=None,headers=None,client=None):
    req=urllib.request.Request(BASE+'api.php?route='+urllib.parse.quote(route,safe=''),data=None if body is None else json.dumps(body).encode(),headers={'Content-Type':'application/json',**(headers or {})})
    try:
        with (client or opener).open(req,timeout=30) as res:return res.status,json.load(res)
    except urllib.error.HTTPError as e:return e.code,json.load(e)
def check(ok,name):
    global checks
    assert ok,name;checks+=1;print('PASS HTTP',name,flush=True)
check(call('/api/data')[0]==401,'unauthenticated data blocked')
check(call('/api/login',{'name':'Manager','code':'bad'})[0]==401,'invalid login rejected')
check(call('/api/login',{'name':'Manager','code':'TestManagerCode-2026'})[0]==200,'management login')
check(all(c.path=='/manhour/' and c.has_nonstandard_attr('HttpOnly') for c in jar),'scoped HttpOnly cookie')
status,data=call('/api/data');check(status==200 and isinstance(data['config']['clientMapping'],dict),'snapshot object maps')
check(data['user']['role']=='admin','management role')
check(call('/api/login',{'name':'Other','code':'TestManagerCode-2026'},{'Origin':'https://other.example'})[0]==403,'cross-origin save blocked')
check(call('/api/login',{'name':'Other','code':'TestManagerCode-2026'},{'Content-Type':'text/plain'})[0]==415,'non-JSON mutation blocked')
check(call('/api/backup')[0]==200,'full JSON backup download')
check(call('/api/status')[1]['imported'] is True,'transfer status visible')
# Two HTTP clients editing the same revision: exactly one succeeds.
rows=data['records'];row=rows[0];week=data['editableFromWeek'];cookie='; '.join(f'{c.name}={c.value}' for c in jar)
def save(row,value):
    r=json.loads(json.dumps(row));r['payload']['weeks'][week]=value
    return call('/api/records',{**r,'requestId':str(uuid.uuid4()),'databaseEpoch':data['databaseEpoch']},{'Cookie':cookie},urllib.request.build_opener())[0]
with concurrent.futures.ThreadPoolExecutor(2) as ex:codes=list(ex.map(lambda n:save(row,n),[10,11]))
check(sorted(codes)==[200,409],'simultaneous same-row saves conflict')
data=call('/api/data')[1]
with concurrent.futures.ThreadPoolExecutor(2) as ex:codes=list(ex.map(lambda pair:save(pair[0],pair[1]),[(data['records'][0],12),(data['records'][1],.9)]))
check(codes==[200,200],'simultaneous different rows both save')
check(call('/api/logout',{})[0]==200 and call('/api/data')[0]==401,'logout invalidates session')
check(call('/api/login',{'name':'Team','code':'TestEditorCode-2026'})[0]==200,'editor login')
check(call('/api/backup')[0]==403,'editor backup restriction')
check(call('/api/members')[0]==403,'editor audit restriction')
call('/api/logout',{});call('/api/login',{'name':'View','code':'TestViewerCode-2026'});view=call('/api/data')[1]
check(view['user']['role']=='viewer','view-only login')
check(call('/api/records',{**view['records'][0],'requestId':str(uuid.uuid4()),'databaseEpoch':view['databaseEpoch']})[0]==403,'viewer save blocked')
with urllib.request.urlopen(BASE+'index.php') as r:page=r.read().decode();check('href="style.css"' in page and 'src="app.js"' in page,'subfolder asset URLs')
with urllib.request.urlopen(BASE+'app.js') as r:check('api.php?route=' in r.read().decode(),'subfolder API mapping bundled')
call('/api/logout',{})
for i in range(15):call('/api/login',{'name':'Unknown','code':'wrong'})
check(call('/api/login',{'name':'Unknown','code':'wrong'})[0]==429,'login throttling')
print(f'Completed {checks} HTTP checks.',flush=True)
