"""
Uji end-to-end Fase 1 (Playwright). Jalankan dari root proyek pada database UJI yang baru diinstal dengan --demo:

    php -S 127.0.0.1:8080 -t public public/index.php &
    python3 tests/e2e_fase1.py <password-sementara-admin>

Variabel NEXORA_PSQL dapat diisi perintah psql (default: su postgres -c "psql -d nexora").
JANGAN jalankan pada database produksi (test mengubah data & mengunci akun uji).
"""
import os, re, sys, json, subprocess
from playwright.sync_api import sync_playwright, expect

BASE = "http://127.0.0.1:8080"
SHOTS = os.environ.get("NEXORA_SHOTS", "/tmp/")
TEMP_PW = sys.argv[1]
NEW_PW = "Rumah-Pintar-Aman-2026"
results = []
def ok(name, cond, info=""):
    results.append((name, bool(cond), info))
    print(("PASS " if cond else "FAIL ") + name + (" — " + str(info) if info else ""))

PSQL = os.environ.get("NEXORA_PSQL", 'su postgres -c "psql -d nexora -tAc \\"{sql}\\""')
def psql(sql):
    return subprocess.run(PSQL.format(sql=sql), shell=True, capture_output=True, text=True).stdout.strip()

with sync_playwright() as p:
    b = p.chromium.launch()
    ctx = b.new_context(viewport={"width": 1440, "height": 900})
    page = ctx.new_page()
    errors = []
    page.on("console", lambda m: errors.append(m.text) if m.type == "error" else None)
    page.on("pageerror", lambda e: errors.append(str(e)))

    # 1. Login + wajib ganti password
    page.goto(BASE + "/login")
    page.fill("#email", "owner@nexora.test"); page.fill("#password", TEMP_PW); page.click("button[type=submit]")
    ok("login redirect ke ganti password", "/password/change" in page.url, page.url)
    page.goto(BASE + "/devices")
    ok("akses lain diblokir sebelum ganti password", "/password/change" in page.url)
    page.evaluate("document.getElementById('password').removeAttribute('minlength')"); page.fill("#current_password", TEMP_PW); page.fill("#password", "pendek"); page.fill("#password_confirm", "pendek"); page.click("form[action='/password/change'] button")
    ok("password lemah ditolak", page.locator(".flash-error").count() > 0)
    page.fill("#current_password", TEMP_PW); page.fill("#password", NEW_PW); page.fill("#password_confirm", NEW_PW); page.click("form[action='/password/change'] button")
    ok("ganti password sukses → beranda", page.url.rstrip("/") == BASE, page.url)
    page.screenshot(path=SHOTS + "01-dashboard.png", full_page=True)

    # 2. Struktur lokasi
    page.goto(BASE + "/structure"); page.screenshot(path=SHOTS + "02-structure.png", full_page=True)
    ok("struktur menampilkan 4 lokasi", page.locator("section.card h2").filter(has_text=re.compile("Lantai|Area")).count() >= 4)

    # 3. Denah Lantai 1
    page.goto(BASE + "/floorplans")
    page.click(".loc-tabs a:has-text('Lantai 1')")
    loc_url = page.url
    page.set_input_files("#plan", "tests/fake.png"); page.click("form[action$='/upload'] button")
    ok("upload file palsu ditolak", "tidak didukung" in page.content() or "bukan gambar" in page.content())
    page.set_input_files("#plan", "tests/denah-contoh.jpg"); page.click("form[action$='/upload'] button")
    ok("upload denah sukses", page.locator(".flash-success").count() > 0)
    page.wait_for_selector(".fp-marker", timeout=5000) if page.locator(".fp-marker").count() else None
    page.click("[data-fp=edit]")
    page.wait_for_selector("[data-fp=palette-list] button")
    n_before = page.locator(".fp-marker").count()
    for name, fx, fy in [("Lampu Ruang Tamu", 0.3, 0.35), ("Pintu Utama", 0.25, 0.75), ("Sensor Pintu Belakang", 0.7, 0.3)]:
        page.click(f"[data-fp=palette-list] button:has-text('{name}')")
        page.locator("[data-fp=viewport]").scroll_into_view_if_needed()
        vp = page.locator("[data-fp=viewport]").bounding_box()
        page.mouse.click(vp["x"] + vp["width"] * fx, vp["y"] + vp["height"] * fy)
        page.wait_for_timeout(900)
    ok("3 perangkat ditempatkan", page.locator(".fp-marker").count() == n_before + 3, page.locator(".fp-marker").count())
    pos_before = psql("SELECT x||','||y FROM placements pl JOIN devices d ON d.id=pl.device_id WHERE d.code='DEV-L1-LGT-001'")
    m = page.locator(".fp-marker").first.bounding_box()
    page.mouse.move(m["x"] + m["width"]/2, m["y"] + m["height"]/2); page.mouse.down()
    page.mouse.move(m["x"] + 120, m["y"] + 40, steps=8); page.mouse.up(); page.wait_for_timeout(800)
    pos_after = psql("SELECT x||','||y FROM placements pl JOIN devices d ON d.id=pl.device_id WHERE d.code='DEV-L1-LGT-001'")
    ok("drag memindahkan simbol & tersimpan", pos_before != pos_after, f"{pos_before} → {pos_after}")
    page.click("[data-fp=undo]"); page.wait_for_timeout(800)
    pos_undo = psql("SELECT x||','||y FROM placements pl JOIN devices d ON d.id=pl.device_id WHERE d.code='DEV-L1-LGT-001'")
    ok("undo mengembalikan posisi", pos_undo == pos_before, pos_undo)
    page.screenshot(path=SHOTS + "03-floorplan-edit.png")
    page.click("[data-fp=edit]")
    page.locator(".fp-marker").first.click(); page.wait_for_timeout(300)
    page.screenshot(path=SHOTS + "04-floorplan-detail.png")

    # posisi relatif tetap setelah resize (AC-02)
    def rel():
        vb = page.locator(".fp-stage img").bounding_box(); mb = page.locator(".fp-marker").first.bounding_box()
        return round((mb["x"] + mb["width"]/2 - vb["x"]) / vb["width"], 3), round((mb["y"] + mb["height"]/2 - vb["y"]) / vb["height"], 3)
    r1 = rel(); page.set_viewport_size({"width": 900, "height": 800}); page.wait_for_timeout(500); r2 = rel()
    ok("posisi relatif tetap saat resize", abs(r1[0]-r2[0]) < 0.01 and abs(r1[1]-r2[1]) < 0.01, f"{r1} vs {r2}")
    page.set_viewport_size({"width": 1440, "height": 900})

    # rotasi mentransformasi koordinat
    page.goto(loc_url)
    xy0 = psql("SELECT round(x,4)||','||round(y,4) FROM placements pl JOIN devices d ON d.id=pl.device_id WHERE d.code='DOOR-MAIN-001'")
    page.click("form[action$='/rotate'] input[value=cw] ~ button")
    xy1 = psql("SELECT round(x,4)||','||round(y,4) FROM placements pl JOIN devices d ON d.id=pl.device_id WHERE d.code='DOOR-MAIN-001'")
    x0, y0 = map(float, xy0.split(",")); x1, y1 = map(float, xy1.split(","))
    ok("rotasi 90° mentransformasi koordinat", abs(x1 - (1 - y0)) < 1e-3 and abs(y1 - x0) < 1e-3, f"{xy0} → {xy1}")

    # 4. API: If-Match & validasi (pakai fetch dari halaman agar sesi/CSRF sama)
    api = page.evaluate("""async () => {
      const csrf = document.querySelector('meta[name=csrf-token]').content;
      const fp = await (await fetch('/api/v1/locations/' + document.getElementById('fp').dataset.locationId + '/floorplan')).json();
      const p = fp.placements[0];
      const h = {'Content-Type':'application/json','X-CSRF-Token':csrf};
      const noMatch = await fetch('/api/v1/placements/'+p.id,{method:'PUT',headers:h,body:JSON.stringify({x:.5,y:.5})});
      const stale = await fetch('/api/v1/placements/'+p.id,{method:'PUT',headers:{...h,'If-Match':'"'+(p.version-1)+'"'},body:JSON.stringify({x:.5,y:.5})});
      const bad = await fetch('/api/v1/placements/'+p.id,{method:'PUT',headers:{...h,'If-Match':'"'+p.version+'"'},body:JSON.stringify({x:2,y:.5})});
      const nocsrf = await fetch('/api/v1/placements/'+p.id,{method:'PUT',headers:{'Content-Type':'application/json','If-Match':'"'+p.version+'"'},body:JSON.stringify({x:.5,y:.5})});
      return {noMatch: noMatch.status, stale: stale.status, staleType: (await stale.json()).type, bad: bad.status, badCt: bad.headers.get('content-type'), nocsrf: nocsrf.status};
    }""")
    ok("PUT tanpa If-Match → 428", api["noMatch"] == 428)
    ok("PUT versi usang → 412 problem+json", api["stale"] == 412 and api["staleType"] == "/problems/version-conflict", api)
    ok("koordinat di luar 0–1 → 422 RFC 9457", api["bad"] == 422 and "problem+json" in api["badCt"])
    ok("tanpa CSRF → ditolak", api["nocsrf"] == 419)

    # 5. Perangkat: bentrok channel & tambah perangkat
    page.goto(BASE + "/devices/new")
    page.fill("#code", "DEV-L1-LGT-002"); page.fill("#name", "Lampu Teras"); page.select_option("#category", "light")
    page.select_option("#protocol", "mqtt"); page.select_option("#controller_id", label="CTRL-L1 — Relay Board Lantai 1 (16 CH)"); page.fill("#channel_no", "1")
    page.click("form[action^='/devices'] button[type=submit]")
    ok("bentrok channel ditolak", "sudah dipakai oleh DEV-L1-LGT-001" in page.content())
    ok("input lama dipertahankan", page.input_value("#code") == "DEV-L1-LGT-002")
    page.select_option("#category", "light"); page.select_option("#protocol", "mqtt")
    page.select_option("#controller_id", label="CTRL-L1 — Relay Board Lantai 1 (16 CH)"); page.fill("#channel_no", "2")
    page.select_option("#location_id", label="Area Luar")
    page.click("form[action^='/devices'] button[type=submit]")
    ok("perangkat baru tersimpan", "/devices/" in page.url and page.locator(".flash-success").count() > 0, page.url)
    page.screenshot(path=SHOTS + "05-device-show.png", full_page=True)
    page.goto(BASE + "/devices"); page.screenshot(path=SHOTS + "06-devices.png", full_page=True)
    page.goto(BASE + "/controllers/1"); page.screenshot(path=SHOTS + "07-controller.png", full_page=True)

    # 6. Anggota: tambah Family dengan scope Lantai 1
    page.goto(BASE + "/members")
    page.fill("#m-email", "keluarga@nexora.test"); page.fill("#m-name", "Anggota Keluarga")
    page.select_option("#m-role", label="Anggota Keluarga")
    page.click("form[action='/members'] button[type=submit]")
    ok("anggota ditambahkan", page.locator(".secret-box").count() == 1)
    fam_pw = re.search(r"Password: (\S+)", page.locator(".secret-box").inner_text()).group(1)
    page.screenshot(path=SHOTS + "08-members.png", full_page=True)

    # 7. Login sebagai Family di konteks terpisah — uji RBAC (AC-10)
    ctx2 = b.new_context(); p2 = ctx2.new_page()
    p2.goto(BASE + "/login"); p2.fill("#email", "keluarga@nexora.test"); p2.fill("#password", fam_pw); p2.click("button[type=submit]")
    p2.fill("#current_password", fam_pw); p2.fill("#password", "Rumah-Bahagia-Sekali-2026"); p2.fill("#password_confirm", "Rumah-Bahagia-Sekali-2026"); p2.click("form[action='/password/change'] button")
    r = p2.goto(BASE + "/devices/new")
    ok("Family tidak bisa tambah perangkat (403)", r.status == 403)
    st = p2.evaluate("""async () => {
      const csrf = document.querySelector('meta[name=csrf-token]').content;
      const r = await fetch('/api/v1/placements',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf},body:JSON.stringify({device_id:1,location_id:1,x:.5,y:.5})});
      return r.status; }""")
    ok("Family tidak bisa ubah denah via API (403)", st == 403)
    r = p2.goto(BASE + "/members"); ok("Family tidak bisa kelola anggota (403)", r.status == 403)
    p2.goto(BASE + "/"); p2.screenshot(path=SHOTS + "09-family-dashboard.png", full_page=True)
    ok("menu konfigurasi tersembunyi untuk Family", p2.locator("a[href='/members']").count() == 0)
    ctx2.close()

    # 8. Lockout akun setelah 5 gagal
    ctx3 = b.new_context(); p3 = ctx3.new_page()
    for i in range(5):
        p3.goto(BASE + "/login"); p3.fill("#email", "keluarga@nexora.test"); p3.fill("#password", "salah-salah-salah"); p3.click("button[type=submit]")
    p3.goto(BASE + "/login"); p3.fill("#email", "keluarga@nexora.test"); p3.fill("#password", "Rumah-Bahagia-Sekali-2026"); p3.click("button[type=submit]")
    ok("akun terkunci setelah 5 gagal (password benar pun ditolak)", "terkunci" in p3.content())
    ctx3.close()

    # 9. MFA setup
    page.goto(BASE + "/profile")
    page.click("form[action='/profile/mfa/start'] button")
    ok("QR TOTP dirender", page.locator("[data-qr] svg").count() == 1)
    secret = page.locator(".secret-box").inner_text().replace(" ", "")
    code = subprocess.run(["php", "-r", f"require 'app/bootstrap.php'; echo App\\Core\\Totp::code('{secret}', intdiv(time(),30));"], capture_output=True, text=True).stdout
    page.fill("#code", code); page.click("form[action='/profile/mfa/confirm'] button")
    ok("MFA aktif", "Aktif" in page.content())
    page.screenshot(path=SHOTS + "10-profile.png", full_page=True)

    # 10. Audit: verifikasi utuh, lalu deteksi manipulasi
    page.goto(BASE + "/audit"); page.click("form[action='/audit/verify'] button")
    ok("audit chain utuh", "Rantai audit utuh" in page.content())
    page.screenshot(path=SHOTS + "11-audit.png", full_page=False)
    upd = psql("UPDATE audit_log SET action='x' WHERE id=3")
    ok("UPDATE audit_log ditolak trigger DB", "append-only" in upd or upd == "", upd)

    js_err = [e for e in errors if "Failed to load resource" not in e]
    ok("tidak ada error JavaScript", not js_err, js_err[:3])
    b.close()

fails = [r for r in results if not r[1]]
print(f"\n{len(results) - len(fails)}/{len(results)} lulus")
sys.exit(1 if fails else 0)
