import subprocess, json, re, tempfile, os

BASE = 'http://127.0.0.1:8115'

def sh(c): return subprocess.run(c, shell=True, capture_output=True, text=True).stdout.strip()

def sql(q):
    # Pass the query via stdin from a temp file rather than a shell -e "..."
    # argument: a query embedding a literal $ (e.g. an Argon2 hash like
    # $argon2id$v=19$...) gets silently shell-expanded as an unset variable
    # (-> empty string) inside double quotes, corrupting it. Stdin sidesteps
    # shell quoting entirely, regardless of what the query contains.
    fd, path = tempfile.mkstemp(suffix='.sql')
    try:
        with os.fdopen(fd, 'w') as f:
            f.write(q)
        return sh(f"mysql -u root zc_safeguarding -N < {path}")
    finally:
        os.remove(path)

ok = fail = 0
def check(name, cond, detail=''):
    global ok, fail
    if cond: ok += 1; print('PASS', name)
    else: fail += 1; print('FAIL', name, detail)

def login_full(email, password, cookies):
    """Full login -> 2FA enrollment -> forced password change. Returns the new password used."""
    sh(f"rm -f {cookies}")
    def get_csrf(url):
        return sh(f"curl -s -b {cookies} -c {cookies} '{url}' | grep -o 'name=\"csrf_token\" value=\"[^\"]*\"' | sed 's/.*value=\"//;s/\"//'")
    csrf = get_csrf(f'{BASE}/admin/login.php')
    sh(f"curl -s -b {cookies} -c {cookies} -o /dev/null -X POST {BASE}/admin/login.php --data-urlencode 'csrf_token={csrf}' --data-urlencode 'email={email}' --data-urlencode 'password={password}'")
    setup_page = sh(f"curl -s -b {cookies} -c {cookies} {BASE}/admin/2fa-setup.php")
    m = re.search(r'secret-display">([^<]+)</div>', setup_page)
    if not m:
        return None, setup_page  # not routed to 2FA setup — caller checks
    secret = m.group(1).replace(' ', '')
    csrf2 = re.search(r'name="csrf_token" value="([^"]+)"', setup_page).group(1)
    code = sh(f"php -r \"require '/home/claude/zc/zc/app/bootstrap.php'; echo App\\\\Services\\\\Totp::currentCode('{secret}');\"")
    sh(f"curl -s -b {cookies} -c {cookies} -o /dev/null -X POST {BASE}/admin/2fa-setup.php --data-urlencode 'csrf_token={csrf2}' --data-urlencode 'code={code}'")
    cp_page = sh(f"curl -s -b {cookies} -c {cookies} {BASE}/admin/change-password.php")
    m3 = re.search(r'name="csrf_token" value="([^"]+)"', cp_page)
    if not m3:
        return None, cp_page
    csrf3 = m3.group(1)
    newpass = 'BrandNew' + email.split('@')[0].replace('.', '') + '456!'
    sh(f"curl -s -b {cookies} -c {cookies} -o /dev/null -X POST {BASE}/admin/change-password.php --data-urlencode 'csrf_token={csrf3}' --data-urlencode 'current_password={password}' --data-urlencode 'new_password={newpass}' --data-urlencode 'confirm_password={newpass}'")
    return newpass, None

def get_csrf(cookies, url):
    # head -1: a page can render the same CSRF token in more than one form
    # (edit.php, cases/view.php), and grep -o returns every match — without
    # head -1 the value ends up with an embedded newline and every POST
    # using it fails CSRF verification.
    return sh(f"curl -s -b {cookies} -c {cookies} '{url}' | grep -o 'name=\"csrf_token\" value=\"[^\"]*\"' | head -1 | sed 's/.*value=\"//;s/\"//'")

# --- Bootstrap: first Super Admin via CLI (as any real deployment would) ---
sh("php /home/claude/zc/zc/cron/create-admin-user.php 'Root Admin' rootadmin@example.com 'TempPassword123!' 'Super Administrator'")
admin_cookies = '/tmp/admin_uf_cookies.txt'
_, err = login_full('rootadmin@example.com', 'TempPassword123!', admin_cookies)
check('root admin bootstrap + full login works', err is None, err)

print('--- Create a new user via the admin UI ---')
csrf = get_csrf(admin_cookies, f'{BASE}/admin/users/create.php')
create_page = sh(f"curl -s -b {admin_cookies} -c {admin_cookies} -X POST {BASE}/admin/users/create.php "
                  f"--data-urlencode 'csrf_token={csrf}' --data-urlencode 'full_name=New Officer' "
                  f"--data-urlencode 'email=newofficer@example.com' --data-urlencode 'phone=263779990001' "
                  f"--data-urlencode 'role_ids[]=' ")  # placeholder, replaced below with real role id
officer_role_id = sql("SELECT id FROM roles WHERE name='Safeguarding Officer'")
csrf = get_csrf(admin_cookies, f'{BASE}/admin/users/create.php')
create_page = sh(f"curl -s -b {admin_cookies} -c {admin_cookies} -X POST {BASE}/admin/users/create.php "
                  f"--data-urlencode 'csrf_token={csrf}' --data-urlencode 'full_name=New Officer' "
                  f"--data-urlencode 'email=newofficer@example.com' --data-urlencode 'phone=263779990001' "
                  f"--data-urlencode 'role_ids[]={officer_role_id}'")
check('temp password shown on the create-success screen', 'Temporary password' in create_page)
m = re.search(r'secret-display">([^<]+)</div>', create_page)
check('a temp password value is actually present', m is not None, create_page[:300])
temp_password = m.group(1) if m else None
new_user_id = sql("SELECT id FROM users WHERE email='newofficer@example.com'")
check('new user row created', new_user_id != '')
check('new user starts status=pending', sql(f"SELECT status FROM users WHERE id={new_user_id}") == 'pending')
check('new user must_change_password=1', sql(f"SELECT must_change_password FROM users WHERE id={new_user_id}") == '1')
check('temp password is NOT stored in plaintext anywhere in the DB',
      temp_password not in sh(f"mysqldump -u root zc_safeguarding 2>/dev/null"))
check('new user has Safeguarding Officer role', sql(f"SELECT r.name FROM roles r JOIN user_roles ur ON ur.role_id=r.id WHERE ur.user_id={new_user_id}") == 'Safeguarding Officer')

print('--- The new (pending) user can actually log in with the temp password ---')
officer_cookies = '/tmp/officer_uf_cookies.txt'
newpass, err = login_full('newofficer@example.com', temp_password, officer_cookies)
check('pending user completes full login (password -> 2FA -> forced password change)', err is None, err)
check('status flips to active after first successful login', sql(f"SELECT status FROM users WHERE id={new_user_id}") == 'active')
dash = sh(f"curl -s -b {officer_cookies} -c {officer_cookies} {BASE}/admin/dashboard.php")
check('new officer reaches the dashboard', 'Welcome, New Officer' in dash, dash[:200])
check('new officer (no users.manage) does NOT see the Manage users panel', 'Manage users' not in dash)

print('--- RBAC: user WITHOUT users.manage cannot create or edit users, even with a valid CSRF token ---')
valid_csrf = get_csrf(officer_cookies, f'{BASE}/admin/dashboard.php')
if not valid_csrf:
    # dashboard has no form; grab one from a page the officer CAN reach that does have one
    valid_csrf = get_csrf(officer_cookies, f'{BASE}/admin/change-password.php')
loc = sh(f"curl -s -b {officer_cookies} -c {officer_cookies} -D - -o /dev/null -X POST {BASE}/admin/users/create.php "
         f"--data-urlencode 'csrf_token={valid_csrf}' --data-urlencode 'full_name=Hacker' --data-urlencode 'email=hacker@example.com' "
         f"--data-urlencode 'role_ids[]={officer_role_id}' | grep -i '^location' | tr -d '\\r'")
check('non-admin POST to create.php redirected away (blocked)', 'login.php' in loc or 'dashboard.php' in loc, loc)
check('no user created by the blocked attempt', sql("SELECT COUNT(*) FROM users WHERE email='hacker@example.com'") == '0')

print('--- Self-lockout guards ---')
# Bootstrap a second Super Admin so we can safely test disabling the FIRST one.
sh("php /home/claude/zc/zc/cron/create-admin-user.php 'Second Admin' secondadmin@example.com 'TempPassword123!' 'Super Administrator'")
second_admin_id = sql("SELECT id FROM users WHERE email='secondadmin@example.com'")
root_admin_id = sql("SELECT id FROM users WHERE email='rootadmin@example.com'")

edit_self_page = sh(f"curl -s -b {admin_cookies} -c {admin_cookies} '{BASE}/admin/users/edit.php?id={root_admin_id}'")
check("edit-self page hides the disable-account form", "You can't change your own account" in edit_self_page)
csrf = get_csrf(admin_cookies, f'{BASE}/admin/users/edit.php?id={root_admin_id}')
resp = sh(f"curl -s -b {admin_cookies} -c {admin_cookies} -X POST {BASE}/admin/users/edit.php?id={root_admin_id} "
          f"--data-urlencode 'csrf_token={csrf}' --data-urlencode 'action=set_status' --data-urlencode 'status=disabled'")
check('server-side rejects a forged self-disable attempt too', 'own account' in resp.lower(), resp[:300])
check('root admin still active in DB (self-disable genuinely blocked)', sql(f"SELECT status FROM users WHERE id={root_admin_id}") == 'active')

# Now disable the SECOND admin from the first admin's session (two active Super Admins exist -> allowed)
csrf = get_csrf(admin_cookies, f'{BASE}/admin/users/edit.php?id={second_admin_id}')
sh(f"curl -s -b {admin_cookies} -c {admin_cookies} -o /dev/null -X POST {BASE}/admin/users/edit.php?id={second_admin_id} "
   f"--data-urlencode 'csrf_token={csrf}' --data-urlencode 'action=set_status' --data-urlencode 'status=disabled'")
check('disabling a NON-last Super Admin succeeds', sql(f"SELECT status FROM users WHERE id={second_admin_id}") == 'disabled')

# Re-enable it, then try to strip Super Administrator role from root admin (now would-be-last) via updateRoles -> must fail
csrf = get_csrf(admin_cookies, f'{BASE}/admin/users/edit.php?id={second_admin_id}')
sh(f"curl -s -b {admin_cookies} -c {admin_cookies} -o /dev/null -X POST {BASE}/admin/users/edit.php?id={second_admin_id} "
   f"--data-urlencode 'csrf_token={csrf}' --data-urlencode 'action=set_status' --data-urlencode 'status=active'")
sh(f"php -r \"require '/home/claude/zc/zc/app/bootstrap.php'; App\\Services\\Database::connection()->prepare('DELETE FROM user_roles WHERE user_id=?')->execute([{second_admin_id}]);\"")
# now only root_admin holds Super Administrator; try to remove it from root_admin via the officer role instead
csrf = get_csrf(admin_cookies, f'{BASE}/admin/users/edit.php?id={root_admin_id}')
resp = sh(f"curl -s -b {admin_cookies} -c {admin_cookies} -X POST {BASE}/admin/users/edit.php?id={root_admin_id} "
          f"--data-urlencode 'csrf_token={csrf}' --data-urlencode 'action=update_roles' --data-urlencode 'role_ids[]={officer_role_id}'")
check('removing Super Administrator from the LAST holder is blocked',
      sql(f"SELECT r.name FROM roles r JOIN user_roles ur ON ur.role_id=r.id WHERE ur.user_id={root_admin_id} AND r.name='Super Administrator'") == 'Super Administrator')

print('--- disabled user cannot log in ---')
sh(f"php -r \"require '/home/claude/zc/zc/app/bootstrap.php'; App\\Services\\Database::connection()->prepare(\\\"UPDATE users SET status='disabled' WHERE id=?\\\")->execute([{new_user_id}]);\"")
dc = '/tmp/disabled_cookies.txt'
sh(f"rm -f {dc}")
csrf = get_csrf(dc, f'{BASE}/admin/login.php')
resp = sh(f"curl -s -b {dc} -c {dc} -X POST {BASE}/admin/login.php --data-urlencode 'csrf_token={csrf}' --data-urlencode 'email=newofficer@example.com' --data-urlencode 'password={newpass}'")
check('disabled user gets a generic invalid-credentials message, not a hint about being disabled',
      'Invalid email or password' in resp)
sh(f"php -r \"require '/home/claude/zc/zc/app/bootstrap.php'; App\\Services\\Database::connection()->prepare(\\\"UPDATE users SET status='active' WHERE id=?\\\")->execute([{new_user_id}]);\"")

print(f"\nRESULT: {ok} passed, {fail} failed")

print('--- Case referrals ---')
# Seed one case + one active safeguarding contact directly, so the test is fast and focused.
sql("""INSERT INTO safeguarding_cases (case_number, is_anonymous, category, status, description_ciphertext, received_at)
       VALUES ('ZC-REFTEST-01', 0, 'Neglect', 'new', NULL, NOW())""")
referral_case_id = sql("SELECT id FROM safeguarding_cases WHERE case_number='ZC-REFTEST-01'")
sql("""INSERT INTO safeguarding_contacts (organisation, contact_type, contact_name, phone, is_active)
       VALUES ('Harare Central Police', 'police', 'Duty Officer', '0771000000', 1)""")
contact_id = sql("SELECT id FROM safeguarding_contacts WHERE organisation='Harare Central Police'")

# root admin (Super Administrator) holds cases.refer via the seeded permission mapping
csrf = get_csrf(admin_cookies, f'{BASE}/admin/cases/view.php?id={referral_case_id}')
resp = sh(f"curl -s -b {admin_cookies} -c {admin_cookies} -X POST {BASE}/admin/cases/view.php?id={referral_case_id} "
          f"--data-urlencode 'csrf_token={csrf}' --data-urlencode 'action=add_referral' "
          f"--data-urlencode 'referral_type=police' --data-urlencode 'contact_id={contact_id}' "
          f"--data-urlencode 'notes=Called and reported the concern.'")
referral_id = sql(f"SELECT id FROM case_referrals WHERE case_id={referral_case_id}")
check('referral row created', referral_id != '')
check('referral linked to the right contact', sql(f"SELECT contact_id FROM case_referrals WHERE id={referral_id}") == contact_id)
check('referral recorded who made it', sql(f"SELECT referred_by FROM case_referrals WHERE id={referral_id}") == root_admin_id)

case_page = sh(f"curl -s -b {admin_cookies} -c {admin_cookies} '{BASE}/admin/cases/view.php?id={referral_case_id}'")
check('referral shows on the case page', 'Harare Central Police' in case_page and 'Police' in case_page)
check('outcome shows as not yet recorded', 'not yet recorded' in case_page)

csrf = get_csrf(admin_cookies, f'{BASE}/admin/cases/view.php?id={referral_case_id}')
sh(f"curl -s -b {admin_cookies} -c {admin_cookies} -o /dev/null -X POST {BASE}/admin/cases/view.php?id={referral_case_id} "
   f"--data-urlencode 'csrf_token={csrf}' --data-urlencode 'action=update_referral_outcome' "
   f"--data-urlencode 'referral_id={referral_id}' --data-urlencode 'outcome=Police opened a case, ref P/1234.'")
check('outcome updated in DB', sql(f"SELECT outcome FROM case_referrals WHERE id={referral_id}") == 'Police opened a case, ref P/1234.')

print('--- RBAC: officer with cases.view+notes only (no cases.refer) cannot refer, even with a valid CSRF token ---')
sql("""INSERT INTO users (uuid, full_name, email, phone, password_hash, status, must_change_password, created_at, updated_at)
       VALUES (UUID(), 'Case Manager Only', 'casemgr@example.com', NULL, 'x', 'disabled', 0, NOW(), NOW())""")
# (status disabled + placeholder hash: this user only needs to exist for the cross-case-tampering
#  and permission checks below, not to actually log in — reuse the already-authenticated `officer_cookies`
#  session instead, which holds Safeguarding Officer... wait that DOES have cases.refer.)
# Use the Case Manager role instead, logged in for real, to test the permission boundary properly.
casemgr_role_id = sql("SELECT id FROM roles WHERE name='Case Manager'")
csrf = get_csrf(admin_cookies, f'{BASE}/admin/users/create.php')
sh(f"curl -s -b {admin_cookies} -c {admin_cookies} -o /dev/null -X POST {BASE}/admin/users/create.php "
   f"--data-urlencode 'csrf_token={csrf}' --data-urlencode 'full_name=Case Mgr' --data-urlencode 'email=realcasemgr@example.com' "
   f"--data-urlencode 'role_ids[]={casemgr_role_id}'")
casemgr_cookies = '/tmp/casemgr_cookies.txt'
casemgr_id = sql("SELECT id FROM users WHERE email='realcasemgr@example.com'")
# Reset the password directly (bootstrapping test access), matching how tests elsewhere seed real logins.
new_hash = sh("php -r \"echo password_hash('TempPassword123!', PASSWORD_ARGON2ID);\"")
sql(f"UPDATE users SET password_hash='{new_hash}' WHERE id={casemgr_id}")
_, err = login_full('realcasemgr@example.com', 'TempPassword123!', casemgr_cookies)
check('case manager test account logs in fully', err is None, err)

valid_csrf = get_csrf(casemgr_cookies, f'{BASE}/admin/cases/view.php?id={referral_case_id}')
loc = sh(f"curl -s -b {casemgr_cookies} -c {casemgr_cookies} -D - -o /dev/null -X POST {BASE}/admin/cases/view.php?id={referral_case_id} "
         f"--data-urlencode 'csrf_token={valid_csrf}' --data-urlencode 'action=add_referral' "
         f"--data-urlencode 'referral_type=police' --data-urlencode 'notes=unauthorized attempt' | grep -i '^location' | tr -d '\\r'")
check("case manager (no cases.refer) redirected away, not processed", 'dashboard.php' in loc, loc)
# Rbac::require() redirects with a flash message rather than rendering it in the
# redirect response itself — confirm the flash actually explains why, on the next page.
dash_after = sh(f"curl -s -b {casemgr_cookies} -c {casemgr_cookies} {BASE}/admin/dashboard.php")
check("flash message names the missing permission", 'cases.refer' in dash_after, dash_after[:300])
check('no unauthorized referral was created', sql(f"SELECT COUNT(*) FROM case_referrals WHERE case_id={referral_case_id}") == '1')

print('--- cross-case tampering: updating a referral via the WRONG case id must fail ---')
sql("""INSERT INTO safeguarding_cases (case_number, is_anonymous, category, status, description_ciphertext, received_at)
       VALUES ('ZC-REFTEST-02', 0, 'Bullying', 'new', NULL, NOW())""")
other_case_id = sql("SELECT id FROM safeguarding_cases WHERE case_number='ZC-REFTEST-02'")
csrf = get_csrf(admin_cookies, f'{BASE}/admin/cases/view.php?id={other_case_id}')
resp = sh(f"curl -s -b {admin_cookies} -c {admin_cookies} -X POST {BASE}/admin/cases/view.php?id={other_case_id} "
          f"--data-urlencode 'csrf_token={csrf}' --data-urlencode 'action=update_referral_outcome' "
          f"--data-urlencode 'referral_id={referral_id}' --data-urlencode 'outcome=tampered'")
check("cross-case referral update rejected (referral belongs to a different case)",
      sql(f"SELECT outcome FROM case_referrals WHERE id={referral_id}") == 'Police opened a case, ref P/1234.')

print(f"\nFINAL RESULT: {ok} passed, {fail} failed")
