"""
Password-reset + SMTP test suite (31 checks). Uses a fake local SMTP server, so no real email is sent.
Needs the same local setup as tests/e2e.py PLUS:
  - 'app_key' set in config/config.php (any 16+ characters)
  - the fake SMTP server running:   python3 tests/fake_smtp.py <output.jsonl> 2525
  - edit SP below to the folder containing mail.jsonl
Run on a freshly imported schema.sql. NEVER against production.
"""
import re, json, subprocess, base64, quopri, email
import requests
B = "http://127.0.0.1:8099"; SP = "/tmp"
res = []
def check(n, c, d=""):
    res.append(c); print(("PASS " if c else "FAIL ") + n + ("" if c else f" -> {d}"))
class S(requests.Session):
    def get(self, u, **k): k.setdefault("allow_redirects", False); return super().get(u, **k)
    def request(self, m, u, **k): k.setdefault("allow_redirects", False); return super().request(m, u, **k)
def sql(q): return subprocess.run(["mariadb","-N","drideas_test","-e",q],capture_output=True,text=True).stdout.strip()
def tok(s, p):
    r = s.get(B+p, allow_redirects=True); m = re.search(r'name="csrf" value="([0-9a-f]+)"', r.text); return m.group(1) if m else None
def post(s, p, d, src, follow=False):
    d = dict(d); d["csrf"] = tok(s, src); return s.post(B+p, data=d, allow_redirects=follow)
def mails():
    return [json.loads(l) for l in open(SP+"/mail.jsonl") if l.strip()]
def body(m):
    msg = email.message_from_string(m["data"]); return msg, msg.get_payload(decode=True).decode()

admin, stu, anon = S(), S(), S()
post(admin, "/login.php", {"email":"admin@test.local","password":"AdminPass12345"}, "/login.php")
post(stu, "/register.php", {"name":"Ama Mensah","email":"ama@test.local","student_id":"","level":"100","password":"OldPassword1","password_confirm":"OldPassword1"}, "/register.php")

# 1. before SMTP is configured
r = anon.get(B+"/forgot_password.php")
check("forgot page says reset unavailable before SMTP is set", "isn't set up yet" in r.text)

# 2. admin page protection
check("email settings blocked for students", stu.get(B+"/admin/email_settings.php").status_code == 403)
check("email settings redirect anonymous", anon.get(B+"/admin/email_settings.php").status_code == 302)

# 3. validation + secure save
r = post(admin, "/admin/email_settings.php", {"action":"save","smtp_host":"bad host!","smtp_port":"0","smtp_encryption":"tls","smtp_user":"","smtp_pass":"","smtp_from_email":"nope","smtp_from_name":""}, "/admin/email_settings.php", follow=True)
check("invalid SMTP settings rejected", "valid SMTP host" in r.text and sql("SELECT COUNT(*) FROM settings WHERE `key`='smtp_host'") == "0")
cfg = {"action":"save","smtp_host":"127.0.0.1","smtp_port":"2525","smtp_encryption":"none","smtp_user":"mailer@drideasstudyhub.com","smtp_pass":"wrong-password","smtp_from_email":"noreply@drideasstudyhub.com","smtp_from_name":"Dr. Ideas Study Hub"}
post(admin, "/admin/email_settings.php", cfg, "/admin/email_settings.php")
stored = sql("SELECT `value` FROM settings WHERE `key`='smtp_pass_enc'")
check("SMTP password stored encrypted (not plaintext)", stored != "" and "wrong-password" not in stored)
r = admin.get(B+"/admin/email_settings.php")
check("saved password never echoed back to the page", "wrong-password" not in r.text and stored not in r.text)

# 4. test email: bad credentials -> clear failure
r = post(admin, "/admin/email_settings.php", {"action":"test","test_to":"admin@test.local"}, "/admin/email_settings.php", follow=True)
check("test email with wrong password reports the SMTP error", "Test failed" in r.text and "bad credentials" in r.text, re.findall(r"Test failed[^<]*", r.text))
check("no mail was delivered on auth failure", len(mails()) == 0)

# 5. correct credentials; leaving password blank keeps the old one
cfg["smtp_pass"] = "right-password"; post(admin, "/admin/email_settings.php", cfg, "/admin/email_settings.php")
cfg["smtp_pass"] = ""; cfg["smtp_from_name"] = "Dr. Ideas Hub Ñ"; post(admin, "/admin/email_settings.php", cfg, "/admin/email_settings.php")
r = post(admin, "/admin/email_settings.php", {"action":"test","test_to":"admin@test.local"}, "/admin/email_settings.php", follow=True)
check("test email succeeds; blank password field kept the saved one", "Test email sent" in r.text, re.findall(r"Test failed[^<]*", r.text))
m = mails()
check("fake server saw AUTH with the decrypted password", m and m[-1]["user"] == "mailer@drideasstudyhub.com" and m[-1]["pass"] == "right-password")
msg, txt = body(m[-1])
check("UTF-8 sender name encoded correctly in headers", "Ñ" in str(email.header.make_header(email.header.decode_header(msg["From"]))), msg["From"])

# 6. forgot password: unknown vs known email look identical
n0 = len(mails())
r1 = post(anon, "/forgot_password.php", {"email":"ghost@nowhere.com"}, "/forgot_password.php", follow=True)
check("unknown email: generic message", "If an account exists" in r1.text)
check("unknown email: no mail sent, no token created", len(mails()) == n0 and sql("SELECT COUNT(*) FROM password_resets") == "0")
r2 = post(anon, "/forgot_password.php", {"email":"ama@test.local"}, "/forgot_password.php", follow=True)
check("known email: identical generic message", "If an account exists" in r2.text)
ms = mails()
check("known email: exactly one mail sent to her", len(ms) == n0 + 1 and "ama@test.local" in ms[-1]["rcpt"][0])
msg, txt = body(ms[-1])
link = re.search(r"(http://127\.0\.0\.1:8099/reset_password\.php\?token=([0-9a-f]{64}))", txt)
check("email contains a 64-hex reset link using the configured app_url", bool(link), txt)
token = link.group(2)
check("DB stores only the SHA-256 hash, not the token", sql("SELECT COUNT(*) FROM password_resets WHERE token_hash='%s'" % token) == "0" and sql("SELECT COUNT(*) FROM password_resets") == "1")

# 7. reset page
check("bad token -> 'not valid'", "Link not valid" in anon.get(B+"/reset_password.php?token="+"0"*64).text)
check("garbage token -> 'not valid'", "Link not valid" in anon.get(B+"/reset_password.php?token=abc").text)
check("good token shows the form", "Choose a new password" in anon.get(B+link.group(1).replace(B,"")).text)
r = post(anon, "/reset_password.php", {"token":token,"new_password":"short","confirm_password":"short"}, link.group(1).replace(B,""), follow=True)
check("short password refused, token still usable", "at least 8" in r.text and sql("SELECT COUNT(*) FROM password_resets WHERE used_at IS NULL") == "1")
r = post(anon, "/reset_password.php", {"token":token,"new_password":"BrandNewPass9","confirm_password":"BrandNewPass9"}, link.group(1).replace(B,""))
check("reset succeeds -> redirect to login", r.status_code == 302 and "login" in r.headers["Location"], r.status_code)
fresh = S(); r = post(fresh, "/login.php", {"email":"ama@test.local","password":"BrandNewPass9"}, "/login.php")
check("can log in with the new password", r.status_code == 302 and "dashboard" in r.headers["Location"])
old = S(); r = post(old, "/login.php", {"email":"ama@test.local","password":"OldPassword1"}, "/login.php", follow=True)
check("old password no longer works", "Incorrect email or password" in r.text)
check("token cannot be reused", "Link not valid" in anon.get(B+"/reset_password.php?token="+token).text)
check("reset is audit-logged", sql("SELECT COUNT(*) FROM audit_logs WHERE action='user.password_reset'") == "1")

# 8. expiry + rate limit + disabled account
post(anon, "/forgot_password.php", {"email":"ama@test.local"}, "/forgot_password.php")
t2 = re.search(r"token=([0-9a-f]{64})", body(mails()[-1])[1]).group(1)
sql("UPDATE password_resets SET expires_at = NOW() - INTERVAL 1 MINUTE")
check("expired token rejected", "Link not valid" in anon.get(B+"/reset_password.php?token="+t2).text)
n = len(mails())
for i in range(4): post(anon, "/forgot_password.php", {"email":"ama@test.local"}, "/forgot_password.php")
check("per-account limit: max 3 reset emails per hour", sql("SELECT COUNT(*) FROM password_resets WHERE user_id=(SELECT id FROM users WHERE email='ama@test.local') AND created_at > NOW() - INTERVAL 1 HOUR") == "3", sql("SELECT COUNT(*) FROM password_resets"))
sql("DELETE FROM password_resets")
sql("UPDATE users SET is_active=0 WHERE email='ama@test.local'"); n = len(mails())
post(anon, "/forgot_password.php", {"email":"ama@test.local"}, "/forgot_password.php")
check("disabled account gets no reset email", len(mails()) == n)
sql("UPDATE users SET is_active=1 WHERE email='ama@test.local'")

# 9. header injection attempt through the email field
n = len(mails())
post(anon, "/forgot_password.php", {"email":"ama@test.local\r\nBcc: attacker@evil.com"}, "/forgot_password.php")
check("CRLF/Bcc injection in email field sends nothing", len(mails()) == n)

# 10. CSRF
check("forgot form requires CSRF token", anon.post(B+"/forgot_password.php", data={"email":"ama@test.local"}).status_code == 419)

print(f"\n{sum(res)}/{len(res)} checks passed")
raise SystemExit(0 if all(res) else 1)
