"""Reproduce five challenges and fixed comparisons over real loopback HTTP."""
import contextlib
import hashlib
import importlib.metadata
import json
from pathlib import Path
import platform
import re
import shutil
import subprocess
import threading
import time
import urllib.error
import urllib.parse
import urllib.request

from werkzeug.serving import make_server, WSGIRequestHandler
from step176_ctf import create_app
from session_demo import demonstrate

ROOT=Path(__file__).resolve().parent


class Quiet(WSGIRequestHandler):
    def log(self,*args,**kwargs):
        pass


@contextlib.contextmanager
def serve(mode='challenge',role='user',language='ko'):
    server=make_server('127.0.0.1',0,create_app(mode,role,language),threaded=True,request_handler=Quiet)
    thread=threading.Thread(target=server.serve_forever,daemon=True);thread.start()
    try:
        yield 'http://127.0.0.1:'+str(server.server_port)
    finally:
        server.shutdown();server.server_close();thread.join(timeout=3)


def get(base,path,method='GET',cookie=None,data=None):
    headers={} if cookie is None else {'Cookie':cookie}
    payload=urllib.parse.urlencode(data).encode() if data is not None else None
    request=urllib.request.Request(base+path,data=payload,headers=headers,method=method)
    try:
        response=urllib.request.urlopen(request,timeout=3)
    except urllib.error.HTTPError as error:
        response=error
    with response:
        return response.status,dict(response.headers),response.read().decode('utf-8')


def flag(body):
    matches=re.findall(r'FLAG\{[a-zA-Z0-9_]+\}',body)
    assert len(matches)==1,matches
    return matches[0]


def test_language(language):
    checks=[];observed=[]
    with serve(language=language) as base:
        status,headers,home=get(base,'/');assert status==200 and f'lang="{language}"' in home
        assert 'href="/enter"' in home and 'action="/submit"' in home
        assert flag(home) in re.search(r'<!--(.*?)-->',home,re.S).group(1)
        flags={1:flag(home)};checks.append('source_comment_and_discoverable_entrance')
        status,headers,robots=get(base,'/robots.txt');assert status==200
        path=re.search(r'Disallow: (/\S+)',robots).group(1)
        status,headers,body=get(base,path);assert status==200;flags[2]=flag(body);checks.append('robots_path_actually_accessible')
        status,head,body=get(base,'/notice',method='HEAD');assert status==200 and body==''
        status,normal,body=get(base,'/notice');assert status==200 and 'FLAG{' not in body
        flags[3]=head['X-Flag'];assert normal['X-Flag']==flags[3];checks.append('head_get_headers_and_body_distinguished')
        status,headers,body=get(base,'/enter');assert status==200 and 'admin=false' in headers['Set-Cookie'] and 'FLAG{' not in body
        status,headers,body=get(base,'/enter',cookie='admin=true');assert status==200;flags[4]=flag(body);checks.append('cookie_tampering_changes_authorization')
        status,headers,body=get(base,'/profile?uid=2');assert status==200 and 'FLAG{' not in body
        status,headers,body=get(base,'/profile?uid=1');assert status==200;flags[5]=flag(body);checks.append('object_id_changes_exposed_profile')
        score=lambda:json.loads(get(base,'/score')[2])
        assert score()['accepted']==0
        get(base,'/submit','POST',data={'challenge':'1','flag':'FLAG{same_shape_wrong_value}'})
        assert score()['accepted']==0;checks.append('flag_shape_is_not_correctness')
        for number,value in flags.items():
            status,headers,body=get(base,'/submit','POST',data={'challenge':str(number),'flag':value})
            assert status==200 and score()['accepted']==number
            observed.append({'challenge':number,'flag':value,'accepted':True})
        assert score()['solved']==[1,2,3,4,5];checks.append('five_exact_flags_submitted')
        get(base,'/submit','POST',data={'challenge':'1','flag':flags[1]});assert score()['accepted']==5;checks.append('duplicate_not_double_counted')
        assert get(base,'/submit','POST',data={'challenge':'9','flag':flags[1]})[0]==400
        assert get(base,'/submit')[0]==405;checks.append('invalid_submission_and_method')
        status,headers,body=get(base,'/profile?uid='+urllib.parse.quote('<svg onload=alert(1)>'))
        assert status==400 and '<svg' not in body;checks.append('unintended_html_injection_removed')
        curl=shutil.which('curl.exe') or shutil.which('curl')
        curl_result={'available':bool(curl),'verified':False}
        if curl:
            response=subprocess.run([curl,'-sS','-I',base+'/notice'],capture_output=True,timeout=5)
            assert response.returncode==0 and flags[3].encode() in response.stdout
            response2=subprocess.run([curl,'-sS','--cookie','admin=true',base+'/enter'],capture_output=True,timeout=5)
            assert response2.returncode==0 and flags[4].encode() in response2.stdout
            curl_result.update(verified=True,headAndCookieCommands=True)
    with serve('fixed','user',language) as base:
        assert 'FLAG{' not in get(base,'/')[2] and 'X-Flag' not in get(base,'/notice')[1]
        assert '/secret_backup' in get(base,'/robots.txt')[2] and get(base,'/secret_backup')[0]==403
        assert get(base,'/enter',cookie='admin=true')[0]==403
        assert get(base,'/profile?uid=1')[0]==403 and get(base,'/profile?uid=2')[0]==200
        assert get(base,'/submit','POST',data={'challenge':'1','flag':flags[1]})[0]==403
        checks.append('fixed_user_denials_and_own_profile_success')
    with serve('fixed','admin',language) as base:
        for path,n in [('/secret_backup',2),('/enter',4),('/profile?uid=1',5)]:
            status,headers,body=get(base,path);assert status==200 and flags[n] in body
        checks.append('fixed_admin_authorized_successes')
    with serve(language=language) as base:
        assert json.loads(get(base,'/score')[2])['accepted']==0
        checks.append('new_server_starts_fresh_score')
    return {'language':language,'checks':checks,'flagsFoundThroughHTTP':observed,'curl':curl_result,
            'externalTargetsContacted':False,'scorePersistent':False}


def main():
    results=[test_language(language) for language in ['ko','en']]
    report={'at':time.time(),'status':'pass','python':platform.python_version(),'platform':platform.platform(),
            'versions':{n:importlib.metadata.version(n) for n in ['Flask','Werkzeug','Jinja2','itsdangerous','MarkupSafe','click','blinker']},
            'languages':results,'signedCookieTestClient':demonstrate(),
            'sources':{n:hashlib.sha256((ROOT/n).read_bytes()).hexdigest() for n in ['step176_ctf.py','session_demo.py','reproduce.py']}}
    print(json.dumps(report,ensure_ascii=False,indent=2))


if __name__=='__main__':
    main()
