"""Phase 9 browser check: password reset (forgot → link → new password), sign-in lockout, two-step sign-in (set up
with a QR/setup key, recovery codes, sign in with a code and with a recovery code), password change, signing out
other devices, staff forced to set up two-step sign-in, workspace message retention, security headers, mobile.
Codes are computed here in Python (RFC 6238), independently of the server code.

Run against a freshly seeded DB, with the API started with REQUIRE_2FA_FOR_PLATFORM=true:
    python e2e/e2e_security.py ./e2e-shots [path/to/backend/data/devwebs.sqlite]
"""
import base64, hashlib, hmac, os, re, secrets, sqlite3, struct, sys, time
from datetime import datetime, timedelta, timezone
from playwright.sync_api import sync_playwright, expect

OUT = sys.argv[1] if len(sys.argv) > 1 else "./e2e-shots"
DB = sys.argv[2] if len(sys.argv) > 2 else os.path.join(os.path.dirname(__file__), "..", "backend", "data", "devwebs.sqlite")
os.makedirs(OUT, exist_ok=True)
BASE = os.environ.get("E2E_BASE", "http://localhost:3000")
PASSWORD = "ChangeMe!2026"
errors = []

def totp(secret_b32, offset=0):
    key = base64.b32decode(secret_b32.replace(" ", "").upper())
    counter = int(time.time() // 30) + offset
    mac = hmac.new(key, struct.pack(">Q", counter), hashlib.sha1).digest()
    o = mac[-1] & 0x0F
    return str((struct.unpack(">I", mac[o:o + 4])[0] & 0x7FFFFFFF) % 1_000_000).zfill(6)

def shot(page, name, full=False):
    page.screenshot(path=f"{OUT}/{name}.png", full_page=full)

def new_page(browser, viewport=(1440, 900), label=""):
    ctx = browser.new_context(viewport={"width": viewport[0], "height": viewport[1]})
    page = ctx.new_page()
    page.on("pageerror", lambda e: errors.append(f"{label} pageerror: {e}"))
    # CSP violations surface as console errors, so this also checks the Content Security Policy.
    page.on("console", lambda m: m.type == "error" and "Failed to load resource" not in m.text and errors.append(f"{label} console: {m.text}"))
    return ctx, page

def password_step(page, email, password=PASSWORD):
    page.goto(f"{BASE}/login")
    page.wait_for_load_state("networkidle")
    page.fill("#email", email); page.fill("#password", password); page.click("button[type=submit]")

with sync_playwright() as p:
    b = p.chromium.launch()

    # ---- Security headers ----
    r = p.request.new_context().get(f"{BASE}/login")
    for h in ["content-security-policy", "x-frame-options", "x-content-type-options", "referrer-policy"]:
        assert h in r.headers, f"missing {h}"
    assert "frame-ancestors 'none'" in r.headers["content-security-policy"]

    # ---- Forgot / reset password ----
    ctx, page = new_page(b, label="reset")
    page.goto(f"{BASE}/login")
    page.get_by_role("link", name="Forgot your password?").click()
    page.wait_for_url("**/forgot-password")
    page.wait_for_load_state("networkidle")
    page.fill("#email", "owner@acme.test")
    page.get_by_role("button", name="Send reset link").click()
    expect(page.get_by_text("If an account exists for")).to_be_visible()
    shot(page, "s01-forgot")
    # Email isn't configured here, so take a fresh link straight from the database (only a hash is stored there).
    token = secrets.token_urlsafe(32)
    db = sqlite3.connect(DB)
    uid = db.execute("select id from users where email='owner@acme.test'").fetchone()[0]
    stamp = lambda d: d.strftime("%Y-%m-%d %H:%M:%S")
    db.execute("insert into password_resets (user_id, token_hash, expires_at, created_at) values (?,?,?,?)",
               (uid, hashlib.sha256(token.encode()).hexdigest(), stamp(datetime.now(timezone.utc) + timedelta(hours=1)), stamp(datetime.now(timezone.utc))))
    db.commit(); db.close()
    page.goto(f"{BASE}/reset-password?token={token}")
    page.fill("#password", "Fresh!Password88"); page.fill("#confirm", "Different!Pass88")
    page.get_by_role("button", name="Change password").click()
    expect(page.get_by_text("The two passwords don't match")).to_be_visible()
    page.fill("#confirm", "Fresh!Password88")
    page.get_by_role("button", name="Change password").click()
    expect(page.get_by_text("Your password has been changed")).to_be_visible()
    page.goto(f"{BASE}/reset-password?token={token}")
    page.fill("#password", "Again!Password88"); page.fill("#confirm", "Again!Password88")
    page.get_by_role("button", name="Change password").click()
    expect(page.get_by_text("invalid or has expired")).to_be_visible()
    password_step(page, "owner@acme.test", "Fresh!Password88")
    page.wait_for_url("**/dashboard")
    ctx.close()

    # ---- Lockout (the reseller account) ----
    ctx, page = new_page(b, label="lockout")
    for _ in range(5):
        password_step(page, "reseller@nexa.test", "wrong-password-1")
        page.wait_for_timeout(250)
    expect(page.get_by_text(re.compile("Too many failed sign-in attempts"))).to_be_visible()
    password_step(page, "reseller@nexa.test")
    expect(page.get_by_text(re.compile("Too many failed sign-in attempts"))).to_be_visible()
    shot(page, "s02-locked")
    ctx.close()

    # ---- Two-step sign-in for a customer owner ----
    ctx, page = new_page(b, label="owner")
    password_step(page, "owner@acme.test", "Fresh!Password88")
    page.wait_for_url("**/dashboard")
    other_ctx, other = new_page(b, label="other-device")
    password_step(other, "owner@acme.test", "Fresh!Password88")
    other.wait_for_url("**/dashboard")

    page.get_by_role("button", name=re.compile("Asha Verma")).click()
    page.get_by_role("link", name="Account security").click()
    page.wait_for_url("**/account/security")
    page.get_by_role("button", name="Turn on").click()
    dlg = page.locator("dialog[open]")
    dlg.locator("#tfa-pw").fill("wrong")
    dlg.get_by_role("button", name="Continue").click()
    expect(dlg.get_by_text("That password is not right")).to_be_visible()
    dlg.locator("#tfa-pw").fill("Fresh!Password88")
    dlg.get_by_role("button", name="Continue").click()
    expect(dlg.get_by_role("img", name=re.compile("QR code"))).to_be_visible()
    secret = dlg.get_by_label("Setup key").inner_text().replace(" ", "")
    shot(page, "s03-2fa-qr")
    dlg.locator("#tfa-code").fill("000000")
    dlg.get_by_role("button", name="Turn on").click()
    expect(dlg.get_by_text("That code is not right")).to_be_visible()
    dlg.locator("#tfa-code").fill(totp(secret))
    dlg.get_by_role("button", name="Turn on").click()
    code_items = page.locator("dialog[open]").get_by_role("list", name="Recovery codes").locator("li")
    expect(code_items).to_have_count(10)
    codes = code_items.all_inner_texts()
    assert len(codes) == 10, codes
    shot(page, "s04-recovery-codes")
    page.locator("dialog[open]").get_by_role("button", name="I've saved them").click()
    expect(page.get_by_text("On", exact=True)).to_be_visible()

    # Sign out the other device from the sessions list.
    expect(page.get_by_text("This device")).to_be_visible()
    page.get_by_role("button", name="Sign out all other devices").click()
    page.locator("dialog[open]").get_by_role("button", name="Sign them out").click()
    expect(page.get_by_text(re.compile(r"Signed out \d+ other device"))).to_be_visible()
    other.goto(f"{BASE}/contacts")
    other.wait_for_url(re.compile(r"/login"), timeout=15000)
    other_ctx.close()

    # Change the password here.
    page.fill("#pw-current", "Fresh!Password88"); page.fill("#pw-new", "Second!Password99"); page.fill("#pw-confirm", "Second!Password99")
    page.get_by_role("button", name="Change password").click()
    expect(page.get_by_text("Password changed")).to_be_visible()
    shot(page, "s05-account-security", full=True)

    # Signing in now needs a code (the next one: the one used to turn it on can't be replayed).
    page.get_by_role("button", name=re.compile("Asha Verma")).click()
    page.get_by_role("button", name="Sign out").click()
    page.wait_for_url("**/login")
    password_step(page, "owner@acme.test", "Second!Password99")
    expect(page.get_by_role("heading", name="Two-step sign-in")).to_be_visible()
    page.fill("#code", "123456")
    page.get_by_role("button", name="Verify and sign in").click()
    expect(page.get_by_text("That code is not right")).to_be_visible()
    shot(page, "s06-code-step")
    page.fill("#code", totp(secret, 1))
    page.get_by_role("button", name="Verify and sign in").click()
    page.wait_for_url("**/dashboard")
    # And with a recovery code.
    ctx2, p2 = new_page(b, label="recovery")
    password_step(p2, "owner@acme.test", "Second!Password99")
    p2.get_by_role("button", name="Use a recovery code").click()
    p2.fill("#code", codes[0])
    p2.get_by_role("button", name="Verify and sign in").click()
    p2.wait_for_url("**/dashboard")
    ctx2.close()

    # ---- Workspace message retention ----
    page.get_by_role("navigation", name="Main").get_by_role("link", name="Workspace").click()
    page.wait_for_url("**/settings/workspace")
    page.locator("#ws-retention").select_option("90")
    page.get_by_role("button", name="Save settings").click()
    dlg = page.locator("dialog[open]")
    expect(dlg.get_by_text("Erase older message content?")).to_be_visible()
    dlg.get_by_role("button", name="Save and erase").click()
    expect(page.get_by_text("Workspace settings saved")).to_be_visible()
    expect(page.locator("#ws-retention")).to_have_value("90")
    shot(page, "s07-workspace")

    # ---- Staff must set up two-step sign-in first ----
    sctx, staff = new_page(b, label="staff")
    password_step(staff, "admin@devwebs.test")
    staff.wait_for_url("**/account/security", timeout=15000)
    expect(staff.get_by_text("Turn on two-step sign-in to continue")).to_be_visible()
    staff.goto(f"{BASE}/admin")
    staff.wait_for_url("**/account/security", timeout=15000)
    shot(staff, "s08-staff-required")
    staff.get_by_role("button", name="Turn on").click()
    dlg = staff.locator("dialog[open]")
    dlg.locator("#tfa-pw").fill(PASSWORD)
    dlg.get_by_role("button", name="Continue").click()
    s_secret = dlg.get_by_label("Setup key").inner_text().replace(" ", "")
    dlg.locator("#tfa-code").fill(totp(s_secret))
    dlg.get_by_role("button", name="Turn on").click()
    expect(staff.locator("dialog[open]").get_by_role("list", name="Recovery codes").locator("li")).to_have_count(10)
    staff.locator("dialog[open]").get_by_role("button", name="I've saved them").click()
    expect(staff.get_by_text("Turn on two-step sign-in to continue")).to_have_count(0)
    expect(staff.get_by_role("button", name="Turn off")).to_have_count(0)  # required for staff
    staff.goto(f"{BASE}/admin")
    expect(staff.get_by_role("heading", name="Platform overview")).to_be_visible()

    # ---- Mobile ----
    mctx = b.new_context(viewport={"width": 390, "height": 844}, storage_state=ctx.storage_state())
    mp = mctx.new_page()
    mp.on("pageerror", lambda e: errors.append(f"mobile pageerror: {e}"))
    for path in ["/account/security", "/settings/workspace"]:
        mp.goto(f"{BASE}{path}")
        mp.wait_for_timeout(900)
        assert mp.evaluate("document.documentElement.scrollWidth <= window.innerWidth"), f"{path} overflows on mobile"
    shot(mp, "s09-mobile-security", full=True)
    lp = b.new_context(viewport={"width": 390, "height": 844}).new_page()
    for path in ["/login", "/forgot-password", "/reset-password?token=x"]:
        lp.goto(f"{BASE}{path}")
        lp.wait_for_timeout(500)
        assert lp.evaluate("document.documentElement.scrollWidth <= window.innerWidth"), f"{path} overflows on mobile"
    b.close()

if errors:
    print("\n".join(errors)); sys.exit(1)
print("security e2e passed")
