"""Integration test backend PHP (butuh server + DB hidup). Otomatis di-skip jika server tidak tersedia.

Siapkan: database diimpor, user uji dibuat (lihat docs/README), lalu
  php -S 127.0.0.1:8080 -t backend-php/public backend-php/router.php
  python3 -m pytest tests/integration -q
"""
import http.cookiejar
import json
import os
import re
import urllib.error
import urllib.parse
import urllib.request

import pytest

BASE = os.environ.get("CSM_BASE_URL", "http://127.0.0.1:8080")
USERS = {"admin": ("admin", os.environ.get("CSM_TEST_ADMIN_PASS", "AdminUji12345")),
         "analyst": ("analis", os.environ.get("CSM_TEST_ANALYST_PASS", "AnalisUji12345")),
         "viewer": ("viewer1", os.environ.get("CSM_TEST_VIEWER_PASS", "ViewerUji12345"))}


class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, *a, **k):
        return None


class Client:
    def __init__(self):
        self.jar = http.cookiejar.CookieJar()
        self.op = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(self.jar), NoRedirect)
        self.csrf = None

    def req(self, path, data=None, headers=None, form=False):
        body = None
        h = dict(headers or {})
        if data is not None:
            body = urllib.parse.urlencode(data).encode() if form else json.dumps(data).encode()
            h.setdefault("Content-Type", "application/x-www-form-urlencoded" if form else "application/json")
        r = urllib.request.Request(BASE + path, data=body, headers=h)
        try:
            resp = self.op.open(r, timeout=10)
        except urllib.error.HTTPError as e:
            resp = e
        raw = resp.read().decode()
        try:
            return resp.status, json.loads(raw)
        except ValueError:
            return resp.status, raw

    def login(self, who):
        _, html = self.req("/login.php")
        token = re.search(r'name="csrf" value="([a-f0-9]+)"', html).group(1)
        u, p = USERS[who]
        st, _ = self.req("/login.php", {"csrf": token, "username": u, "password": p}, form=True)
        assert st == 302, f"login {who} gagal (status {st})"
        _, html = self.req("/dashboard.php")
        self.csrf = re.search(r'name="csrf" content="([a-f0-9]+)"', html).group(1)
        return self

    def get(self, action, **params):
        return self.req("/api.php?" + urllib.parse.urlencode({"action": action, **params}))

    def post(self, action, data, csrf=True):
        return self.req(f"/api.php?action={action}", data, {"X-CSRF-Token": self.csrf} if csrf else {})


def _server_up():
    try:
        urllib.request.urlopen(BASE + "/login.php", timeout=3)
        return True
    except Exception:
        return False


pytestmark = pytest.mark.skipif(not _server_up(), reason="server PHP tidak berjalan")


@pytest.fixture(scope="module")
def admin(): return Client().login("admin")
@pytest.fixture(scope="module")
def analyst(): return Client().login("analyst")
@pytest.fixture(scope="module")
def viewer(): return Client().login("viewer")


def test_unauthenticated_api_is_rejected():
    assert Client().get("overview")[0] == 401
    assert Client().req("/dashboard.php")[0] == 302


def test_wrong_password_rejected():
    c = Client()
    _, html = c.req("/login.php")
    token = re.search(r'name="csrf" value="([a-f0-9]+)"', html).group(1)
    st, body = c.req("/login.php", {"csrf": token, "username": "admin", "password": "salah-banget"}, form=True)
    assert st == 200 and "salah" in body.lower()


def test_login_requires_csrf():
    st, body = Client().req("/login.php", {"username": "admin", "password": USERS["admin"][1]}, form=True)
    assert st == 200 and "tidak valid" in body


@pytest.mark.parametrize("action", ["overview", "events", "attacks", "logins", "network", "fim", "health", "alerts", "incidents", "reports"])
def test_viewer_can_read_dashboard_data(viewer, action):
    st, d = viewer.get(action)
    assert st == 200 and "error" not in d


def test_overview_has_kpis(admin):
    _, d = admin.get("overview")
    assert d["kpi"]["events_today"] >= 0 and d["kpi"]["sensors_total"] >= 3 and d["dist"]


def test_role_enforcement(viewer, analyst):
    assert viewer.get("users")[0] == 403 and viewer.get("settings")[0] == 403 and viewer.get("audit")[0] == 403
    assert viewer.get("sensors")[0] == 403
    assert analyst.get("users")[0] == 403 and analyst.get("sensors")[0] == 200
    assert viewer.post("incident_create", {"title": "x"})[0] == 403
    assert viewer.post("alert_read", {"all": True})[0] == 403


def test_post_requires_csrf(admin):
    assert admin.post("alert_read", {"all": True}, csrf=False)[0] == 403


def test_filters_and_sql_injection_safe(admin):
    st, d = admin.get("events", risk="critical")
    assert st == 200 and all(r["risk_level"] == "critical" for r in d["rows"]) and d["rows"]
    st, d = admin.get("events", ip="' OR '1'='1")
    assert st == 200 and d["rows"] == []                       # diperlakukan sebagai teks biasa
    st, d = admin.get("events", category="sql_injection", risk="high")
    assert st == 200
    assert admin.get("reports", period="yearly")[0] == 422


def test_incident_lifecycle(analyst):
    _, ev = analyst.get("events", category="file_tampering")
    eid = ev["rows"][0]["id"]
    st, r = analyst.post("incident_create", {"event_id": eid, "title": "Uji: sshd_config berubah", "chronology": "uji"})
    if st == 409:   # sudah dibuat pada run sebelumnya
        _, inc = analyst.get("incidents")
        iid = next(i["id"] for i in inc["rows"] if i["event_id"] == eid)
    else:
        assert st == 201 and r["incident_code"].startswith("INC-")
        _, inc = analyst.get("incidents")
        iid = next(i["id"] for i in inc["rows"] if i["incident_code"] == r["incident_code"])
    assert analyst.post("incident_update", {"id": iid, "status": "resolved", "chronology": "k", "actions_taken": "rollback"})[0] == 200
    assert analyst.post("incident_update", {"id": iid, "status": "hacked"})[0] == 422


def test_settings_validation_and_config_alert(admin):
    assert admin.post("settings_update", {"settings": {"brute_force_threshold": "abc"}})[0] == 422
    assert admin.post("settings_update", {"settings": {"brute_force_threshold": "99999"}})[0] == 422
    assert admin.post("settings_update", {"settings": {"nama_ngawur": "1"}})[0] == 422
    _, before = admin.get("alerts")
    st, r = admin.post("settings_update", {"settings": {"brute_force_threshold": "5"}})
    assert st == 200 and r["changed"] == ["brute_force_threshold"]
    _, after = admin.get("alerts")
    assert after["rows"][0]["message"].startswith("Konfigurasi keamanan diubah")


def test_user_management_validation(admin):
    assert admin.post("user_create", {"username": "a b", "email": "x@y.id", "password": "Password12345", "role": "viewer"})[0] == 422
    assert admin.post("user_create", {"username": "uji_user", "email": "bukan-email", "password": "Password12345", "role": "viewer"})[0] == 422
    assert admin.post("user_create", {"username": "uji_user", "email": "u@y.id", "password": "pendek1", "role": "viewer"})[0] == 422
    assert admin.post("user_create", {"username": "uji_user", "email": "u@y.id", "password": "Password12345", "role": "root"})[0] == 422
    _, me = admin.get("users")
    admin_id = next(u["id"] for u in me["rows"] if u["username"] == "admin")
    assert admin.post("user_toggle", {"id": admin_id})[0] == 422   # tidak bisa menonaktifkan diri sendiri
    # password tidak pernah dikembalikan
    assert all("password" not in k for u in me["rows"] for k in u)


def test_sensor_create_returns_key_once(admin):
    import secrets
    code = "uji-" + secrets.token_hex(3)
    st, r = admin.post("sensor_create", {"sensor_code": code, "os": "linux"})
    assert st == 201 and len(r["api_key"]) == 48
    _, lst = admin.get("sensors")
    assert all("api_key" not in x and "api_key_hash" not in x for x in lst["rows"])
    assert admin.post("sensor_create", {"sensor_code": code, "os": "linux"})[0] == 409
    assert admin.post("sensor_create", {"sensor_code": "x y", "os": "linux"})[0] == 422


def test_audit_trail_records_actions(admin):
    _, a = admin.get("audit")
    actions = {r["action"] for r in a["rows"]}
    assert {"login", "settings_update"} <= actions
