#!/usr/bin/env python3
"""Run from the DDEV WordPress root. Local-only: temporarily enables custom login.
Restores the complete settings option and deletes its disposable subscriber.
"""
import os, subprocess, json, ssl, urllib.request, urllib.parse, http.cookiejar, re, secrets

def wp(*args):
    return subprocess.check_output((['wp'] if os.environ.get('A360_TEST_IN_CONTAINER') == '1' else ['ddev', 'wp']) + list(args), text=True).strip()

def check(ok, label):
    assert ok, label
    print('PASS:', label)

base = wp('option', 'get', 'home')
assert '.ddev.site' in base, 'This test is restricted to DDEV.'
original = wp('option', 'get', 'a360_login_avatar_panel', '--format=json')
settings = json.loads(original)
slug = settings.get('custom_login_slug') or 'a360-access-test'
url = base + '/' + slug + '/'
jar = http.cookiejar.CookieJar()
class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, *args):
        return None
opener = urllib.request.build_opener(urllib.request.HTTPSHandler(context=ssl._create_unverified_context()), urllib.request.HTTPCookieProcessor(jar), NoRedirect())
def request(path, data=None):
    req = urllib.request.Request(urllib.parse.urljoin(base, path), None if data is None else urllib.parse.urlencode(data).encode())
    try:
        r = opener.open(req)
    except urllib.error.HTTPError as e:
        r = e
    return r.code, r.headers, r.read().decode(errors='replace')
uid = None
try:
    # Avoid repeated smoke runs triggering the normal IP lockout.
    wp('option', 'patch', 'update', 'a360_login_avatar_panel', 'ip_lockout_threshold', '9999')
    wp('option', 'patch', 'update', 'a360_login_avatar_panel', 'ip_email_threshold', '9999')
    wp('option', 'patch', 'update', 'a360_login_avatar_panel', 'custom_login_enabled', '0')
    check(request(base + '/wp-login.php')[0] == 200, 'disabled: core login available')
    code, headers, _ = request(base + '/wp-admin/')
    check(code == 302 and 'wp-login.php' in headers.get('Location', ''), 'disabled: core admin redirect preserved')
    wp('option', 'patch', 'update', 'a360_login_avatar_panel', 'custom_login_slug', slug)
    wp('option', 'patch', 'update', 'a360_login_avatar_panel', 'custom_login_enabled', '1')
    code, headers, body = request(url)
    check(code == 200 and 'id="loginform"' in body, 'custom GET renders login')
    check('no-cache' in headers.get('Cache-Control', ''), 'login is not cached')
    check(re.search(r'<form[^>]+action="' + re.escape(url), body), 'login POST targets custom URL')
    for path in ['/wp-admin/', '/wp-admin/index.php', '/wp-admin/profile.php',
                 '/wp-admin/admin.php?page=test', '/wp-admin/options.php',
                 '/wp-admin/index.php?doing_ajax=1']:
        for data in [None, {'action': 'test'}]:
            code, headers, body = request(base + path, data)
            check(code == 404 and 'Location' not in headers and slug not in body,
                  'guest admin hidden without revealing slug: ' + path + (' POST' if data else ' GET'))
            check('no-cache' in headers.get('Cache-Control', ''), 'admin 404 is not cached')
    for script in ['admin-ajax.php', 'admin-post.php']:
        for data in [None, {'action': 'a360_unknown_test_action'}]:
            suffix = '?action=a360_unknown_test_action' if data is None else ''
            code, headers, _ = request(base + '/wp-admin/' + script + suffix, data)
            check(code == 400 and 'Location' not in headers, 'public handler reaches core: ' + script)
    check(request(base + '/wp-login.php')[0] == 404, 'core login hidden')
    check(request(base + '/wp-login.php', {'log':'', 'pwd':''})[0] == 404, 'core POST hidden')
    code, _, body = request(url, {'log':'', 'pwd':''})
    check(code == 200 and 'loginform' in body, 'empty login POST handled by core')
    code, _, body = request(url + '?action=lostpassword')
    check(code == 200 and 'lostpasswordform' in body and url in body, 'lost password GET')
    check(request(url + '?action=lostpassword', {'user_login':''})[0] == 200, 'lost password POST validation')
    check(request(url + '?action=rp&key=invalid&login=invalid')[0] == 302, 'invalid reset token redirects')
    name = 'a360_test_' + secrets.token_hex(5)
    password = secrets.token_urlsafe(24)
    uid = wp('user', 'create', name, name + '@example.invalid', '--role=subscriber', '--user_pass=' + password, '--porcelain')
    # Assign an avatar to ensure the moved endpoint cannot bypass existing checks.
    wp('option', 'patch', 'insert', 'a360_login_avatar_panel', 'users', uid, 'gift')
    code, _, body = request(url, {'log':name, 'pwd':password})
    check(code == 200 and 'loginform' in body, 'correct password without avatar remains denied')
    code, _, body = request(url)
    nonce = re.search(r'name="a360_login_avatar_nonce" value="([^"]+)"', body).group(1)
    code, headers, _ = request(url, {'log':name, 'pwd':password, 'a360_login_avatar_nonce':nonce, 'a360_login_avatar':'gift', 'testcookie':'1', 'redirect_to':base + '/wp-admin/profile.php'})
    check(code == 302 and '/wp-admin/profile.php' in headers.get('Location',''), 'valid POST sets session and preserves redirect')
    code, headers, body = request(base + '/wp-admin/profile.php')
    for _ in range(5):
        if code not in (301, 302, 303):
            break
        code, headers, body = request(urllib.parse.urljoin(base, headers['Location']))
    if 'action=logout' not in body:
        print('Admin response:', code, headers.get('Location'), body[:250])
    check(code == 200, 'authenticated profile remains accessible')
    logout = re.search(r"""href=["']([^"']*action=logout[^"']*)["']""", body)
    import html
    check(logout is not None, 'authenticated admin exposes logout')
    logout_url = html.unescape(logout.group(1))
    check(slug in logout_url and '_wpnonce=' in logout_url, 'logout URL preserves nonce')
    check(request(logout_url)[0] == 302, 'nonce-protected logout redirects')
    check(request(base + '/wp-admin/')[0] == 404, 'admin hidden again after logout')
    # Real reset token: core issues path-scoped reset cookie then renders POST form.
    key = wp('eval', 'echo get_password_reset_key(get_user_by("id", ' + uid + '));')
    jar.clear()
    code, headers, _ = request(url + '?action=rp&key=' + key + '&login=' + name)
    check(code == 302 and any(c.name.startswith('wp-resetpass-') for c in jar), 'reset token creates reset cookie')
    code, _, body = request(headers['Location'])
    check(code == 200 and 'resetpassform' in body, 'valid reset token renders reset form')
    newpass = secrets.token_urlsafe(24)
    code, _, body = request(url + '?action=resetpass', {'pass1':newpass, 'pass2':newpass, 'rp_key':key})
    check(code == 200 and wp('eval', 'echo wp_check_password(' + json.dumps(newpass) + ', get_userdata(' + uid + ')->user_pass) ? "yes" : "no";') == 'yes', 'reset password POST changes disposable password')
finally:
    wp('option', 'update', 'a360_login_avatar_panel', original, '--format=json')
    if uid:
        wp('user', 'delete', uid, '--yes')
    check(json.loads(wp('option','get','a360_login_avatar_panel','--format=json')) == json.loads(original), 'original settings restored')
