"""Pengujian backend: login, DB, registrasi, status online/offline, perintah, kebijakan, download approval, notifikasi, audit, RBAC."""
import json
import time

import pytest
from conftest import ADMIN, H, register_agent

from app.models import ActivityLog
from app.security import sign_command, verify_password


def test_01_setup_only_once_and_login(client):
    assert client.get("/api/setup/status").json()["needs_setup"] is False
    r = client.post("/api/setup", json={"setup_code": "XXXX", "username": "evil", "full_name": "Evil", "password": "Abcdefg123"})
    assert r.status_code == 409
    bad = client.post("/api/auth/login", json={"username": "admin", "password": "salah"})
    assert bad.status_code == 401
    ok = client.post("/api/auth/login", json=ADMIN)
    assert ok.status_code == 200 and ok.json()["user"]["role"] == "admin"


def test_02_database_connection_and_argon2(client):
    assert client.get("/api/health").json()["status"] == "ok"
    from app.database import SessionLocal
    from app.models import Administrator
    with SessionLocal() as db:
        a = db.query(Administrator).first()
        assert a.password_hash.startswith("$argon2id$")
        assert verify_password(ADMIN["password"], a.password_hash)


def test_03_refresh_rotation_and_reuse_detection(client):
    login = client.post("/api/auth/login", json=ADMIN).json()
    r1 = client.post("/api/auth/refresh", json={"refresh_token": login["refresh_token"]})
    assert r1.status_code == 200
    reuse = client.post("/api/auth/refresh", json={"refresh_token": login["refresh_token"]})
    assert reuse.status_code == 401
    assert client.post("/api/auth/refresh", json={"refresh_token": r1.json()["refresh_token"]}).status_code == 401  # seluruh sesi dicabut


def test_04_agent_registration_requires_key(client, enroll_key):
    r = client.post("/api/agent/register", json={"enrollment_key": "salah", "device_uid": "bbbbbbbb-9999", "hostname": "X"})
    assert r.status_code == 403
    info = register_agent(client, enroll_key)
    assert info["token"] and info["signing_key"]
    comps = client.get("/api/computers", headers=H(client.admin_token)).json()
    assert any(c["hostname"] == "PC-01" for c in comps)


def test_05_online_offline_status_and_notifications(client, admin, enroll_key):
    info = register_agent(client, enroll_key, "aaaaaaaa-0002", "PC-02")
    cid = info["computer_id"]
    with client.websocket_connect("/ws/agent", headers={"Authorization": f"Bearer {info['token']}"}) as ws:
        first = json.loads(ws.receive_text())
        assert first["type"] == "policy_update" and first["policy"]["internet"]["mode"] in ("blocklist", "allow_all")
        ws.send_text(json.dumps({"type": "heartbeat", "state": "in_use", "user": "murid02", "metrics": {"cpu": 12.5, "ram_percent": 40},
                                 "policy_version": first["policy"]["version"], "ip": "192.168.1.12"}))
        time.sleep(0.3)
        c = client.get(f"/api/computers/{cid}", headers=admin).json()
        assert c["is_online"] and c["usage_state"] == "in_use" and c["metrics"]["cpu"] == 12.5
        assert client.get("/api/stats/overview", headers=admin).json()["online"] >= 1
    time.sleep(0.3)
    assert client.get(f"/api/computers/{cid}", headers=admin).json()["is_online"] is False
    notes = client.get("/api/notifications", headers=admin).json()
    assert any(n["type"] == "computer_offline" for n in notes) and any(n["type"] == "computer_new" for n in notes)


def test_06_invalid_device_token_rejected(client):
    with pytest.raises(Exception):
        with client.websocket_connect("/ws/agent", headers={"Authorization": "Bearer palsu"}) as ws:
            ws.receive_text()
    assert client.get("/api/agent/policy", headers=H("palsu")).status_code == 401


def test_07_commands_confirmation_signature_and_offline(client, admin, enroll_key):
    info = register_agent(client, enroll_key, "aaaaaaaa-0003", "PC-03")
    cid = info["computer_id"]
    # tindakan berisiko tanpa konfirmasi ditolak
    r = client.post("/api/commands", headers=admin, json={"computer_ids": [cid], "action": "shutdown"})
    assert r.status_code == 409 and r.json()["detail"]["code"] == "confirmation_required"
    # offline -> gagal dengan alasan jelas
    r = client.post("/api/commands", headers=admin, json={"computer_ids": [cid], "action": "shutdown", "confirm": True, "reason": "uji"})
    assert r.json()["results"][0]["status"] == "failed" and "offline" in r.json()["results"][0]["result"].lower()
    with client.websocket_connect("/ws/agent", headers={"Authorization": f"Bearer {info['token']}"}) as ws:
        ws.receive_text()
        r = client.post("/api/commands", headers=admin, json={"computer_ids": [cid], "action": "restart", "confirm": True, "reason": "uji"})
        cmd = json.loads(ws.receive_text())
        assert cmd["type"] == "command" and cmd["action"] == "restart"
        payload = {k: cmd[k] for k in ("id", "action", "params", "nonce", "exp")}
        assert cmd["sig"] == sign_command("aaaaaaaa-0003", payload)
        ws.send_text(json.dumps({"type": "command_result", "id": cmd["id"], "ok": True, "detail": "shutdown /r dijalankan"}))
        time.sleep(0.3)
    hist = client.get("/api/commands", headers=admin).json()
    assert any(h["action"] == "restart" and h["status"] == "success" for h in hist)
    # pesan wajib berisi teks
    assert client.post("/api/commands", headers=admin, json={"computer_ids": [cid], "action": "message", "params": {"text": " "}}).status_code == 422
    # kill_process hanya .exe tanpa path
    assert client.post("/api/commands", headers=admin, json={"computer_ids": [cid], "action": "kill_process", "confirm": True,
                                                              "params": {"name": "..\\evil.exe"}}).status_code == 422


def test_08_internet_policies_whitelist_blacklist(client, admin, enroll_key):
    info = register_agent(client, enroll_key, "aaaaaaaa-0004", "PC-04")
    cid = info["computer_id"]
    bad = client.post("/api/policies/internet", headers=admin, json={"name": "Salah", "mode": "blocklist", "blocked": ["bukan domain!!"]})
    assert bad.status_code == 422
    p = client.post("/api/policies/internet", headers=admin, json={"name": "Uji Blok", "mode": "blocklist", "block_social": True,
                                                                     "blocked": ["https://www.contoh-game.com/halaman"],
                                                                     "allowed": ["classroom.google.com", "youtube.com"], "block_entertainment": True}).json()
    assert p["blocked"] == ["contoh-game.com"]
    assert client.post("/api/assignments", headers=admin, json={"policy_type": "internet", "policy_id": p["id"], "target_type": "computer", "target_id": cid}).status_code == 200
    pol = client.get("/api/agent/policy", headers=H(info["token"])).json()["internet"]
    assert pol["mode"] == "blocklist"
    assert "tiktok.com" in pol["block_domains"] and "contoh-game.com" in pol["block_domains"]
    assert "youtube.com" not in pol["block_domains"] and "youtube.com" in pol["allow_domains"]  # whitelist menang atas kategori
    assert "vpnbook.com" in pol["block_domains"]
    # blokir total lewat override cepat
    client.patch(f"/api/computers/{cid}", headers=admin, json={"internet_override": "block_all"})
    assert client.get("/api/agent/policy", headers=H(info["token"])).json()["internet"]["mode"] == "block_all"
    client.patch(f"/api/computers/{cid}", headers=admin, json={"internet_override": "none"})
    # allowlist
    only = next(x for x in client.get("/api/policies/internet", headers=admin).json() if x["mode"] == "allowlist")
    client.post("/api/assignments", headers=admin, json={"policy_type": "internet", "policy_id": only["id"], "target_type": "computer", "target_id": cid})
    pol = client.get("/api/agent/policy", headers=H(info["token"])).json()["internet"]
    assert pol["mode"] == "allowlist" and "classroom.google.com" in pol["allow_domains"] and "tiktok.com" not in pol["allow_domains"]


def test_09_browser_policy_tab_limit_and_exam_mode(client, admin, enroll_key):
    info = register_agent(client, enroll_key, "aaaaaaaa-0005", "PC-05")
    cid = info["computer_id"]
    p = client.post("/api/policies/browser", headers=admin, json={"name": "Maks 3 Tab", "max_tabs": 3, "allowed_browsers": ["chrome"], "homepage": "https://classroom.google.com"}).json()
    client.post("/api/assignments", headers=admin, json={"policy_type": "browser", "policy_id": p["id"], "target_type": "computer", "target_id": cid})
    b = client.get("/api/agent/policy", headers=H(info["token"])).json()["browser"]
    assert b["max_tabs"] == 3 and b["allowed_browsers"] == ["chrome"] and b["downloads_require_approval"] is True
    assert client.post("/api/policies/browser", headers=admin, json={"name": "X", "max_tabs": 99}).status_code == 422
    assert client.post("/api/policies/browser", headers=admin, json={"name": "Y", "homepage": "ftp://x"}).status_code == 422
    client.patch(f"/api/computers/{cid}", headers=admin, json={"exam_lock": True})
    pol = client.get("/api/agent/policy", headers=H(info["token"])).json()
    assert pol["browser"]["exam_mode"] is True and pol["internet"]["mode"] == "allowlist" and pol["exam_lock"] is True
    client.patch(f"/api/computers/{cid}", headers=admin, json={"exam_lock": False})


def test_10_download_approval_flow(client, admin, enroll_key):
    info = register_agent(client, enroll_key, "aaaaaaaa-0006", "PC-06")
    tok = H(info["token"])
    body = {"file_name": "tugas.pdf", "file_size": 120000, "file_type": "application/pdf", "source_url": "https://sekolah.sch.id/tugas.pdf", "browser": "chrome"}
    with client.websocket_connect("/ws/agent", headers={"Authorization": f"Bearer {info['token']}"}) as ws:
        ws.receive_text()
        r = client.post("/api/agent/downloads", headers=tok, json=body).json()
        assert r["status"] == "menunggu"
        assert client.post("/api/agent/downloads", headers=tok, json=body).json()["id"] == r["id"]  # tidak dobel
        pend = client.get("/api/downloads?status=menunggu", headers=admin).json()
        assert any(x["id"] == r["id"] and x["file_name"] == "tugas.pdf" and x["file_size"] == 120000 for x in pend)
        assert client.get("/api/stats/overview", headers=admin).json()["pending_downloads"] >= 1
        ok = client.post(f"/api/downloads/{r['id']}/approve", headers=admin, json={"minutes": 3, "note": "untuk tugas"})
        assert ok.status_code == 200 and ok.json()["status"] == "disetujui"
        msg = json.loads(ws.receive_text())
        assert msg["type"] == "download_decision" and msg["status"] == "disetujui" and msg["valid_minutes"] == 3
        assert client.post(f"/api/downloads/{r['id']}/reject", headers=admin, json={}).status_code == 409  # sudah diputuskan
        st = client.get(f"/api/agent/downloads/{r['id']}", headers=tok).json()
        assert st["status"] == "disetujui" and st["used"] is False
        assert client.post(f"/api/agent/downloads/{r['id']}/used", headers=tok).status_code == 200
        assert client.post(f"/api/agent/downloads/{r['id']}/used", headers=tok).status_code == 409  # izin sekali pakai
    # ditolak
    r2 = client.post("/api/agent/downloads", headers=tok, json={**body, "file_name": "game.zip", "source_url": "https://x.com/game.zip"}).json()
    rej = client.post(f"/api/downloads/{r2['id']}/reject", headers=admin, json={"note": "bukan untuk belajar"}).json()
    assert rej["status"] == "ditolak"
    # aturan otomatis: .exe langsung ditolak sistem
    r3 = client.post("/api/agent/downloads", headers=tok, json={**body, "file_name": "setup.exe", "source_url": "https://x.com/setup.exe"}).json()
    assert r3["status"] == "ditolak" and r3["auto_decision"] is True
    # kedaluwarsa
    from datetime import timedelta
    from app.database import SessionLocal
    from app.models import DownloadRequest, utcnow
    r4 = client.post("/api/agent/downloads", headers=tok, json={**body, "file_name": "lama.pdf", "source_url": "https://x.com/lama.pdf"}).json()
    with SessionLocal() as db:
        db.get(DownloadRequest, r4["id"]).expires_at = utcnow() - timedelta(minutes=1)
        db.commit()
    assert client.post(f"/api/downloads/{r4['id']}/approve", headers=admin, json={}).status_code == 409
    assert client.get(f"/api/agent/downloads/{r4['id']}", headers=tok).json()["status"] == "kedaluwarsa"
    # antarmuka agent tidak bisa membaca permintaan komputer lain
    other = register_agent(client, enroll_key, "aaaaaaaa-0007", "PC-07")
    assert client.get(f"/api/agent/downloads/{r['id']}", headers=H(other["token"])).status_code == 404


def test_11_application_request_flow(client, admin, enroll_key):
    info = register_agent(client, enroll_key, "aaaaaaaa-0008", "PC-08")
    tok = H(info["token"])
    r = client.post("/api/agent/app-requests", headers=tok, json={"app_name": "Python 3.13 Setup", "detail": "C:\\Users\\murid\\Downloads\\python.exe"}).json()
    assert r["status"] == "menunggu"
    with client.websocket_connect("/ws/agent", headers={"Authorization": f"Bearer {info['token']}"}) as ws:
        ws.receive_text()
        ok = client.post(f"/api/app-requests/{r['id']}/approve", headers=admin, json={"minutes": 20}).json()
        assert ok["status"] == "disetujui" and ok["permit_minutes"] == 20
        m = json.loads(ws.receive_text())
        assert m["type"] == "install_permit" and m["minutes"] == 20
    inv = client.post("/api/agent/inventory", headers=tok, json={"applications": [{"name": "Google Chrome", "version": "130"}, {"name": "Notepad++", "version": "8"}]})
    assert inv.json()["count"] == 2
    inv2 = client.post("/api/agent/inventory", headers=tok, json={"applications": [{"name": "Google Chrome", "version": "130"}, {"name": "Notepad++", "version": "8"}, {"name": "Roblox", "version": "1"}]})
    assert inv2.json()["added"] == 1
    assert any(n["type"] == "app_installed" for n in client.get("/api/notifications", headers=admin).json())
    assert len(client.get(f"/api/computers/{info['computer_id']}/applications", headers=admin).json()) == 3


def test_12_events_and_notifications(client, admin, enroll_key):
    info = register_agent(client, enroll_key, "aaaaaaaa-0009", "PC-09")
    r = client.post("/api/agent/events", headers=H(info["token"]), json={"type": "blocked_site", "detail": {"domain": "tiktok.com", "browser": "chrome"}})
    assert r.status_code == 200
    assert client.post("/api/agent/events", headers=H(info["token"]), json={"type": "blocked_site", "detail": {"domain": "tiktok.com"}}).json().get("debounced")
    assert any(n["type"] == "blocked_site" for n in client.get("/api/notifications", headers=admin).json())
    rep = client.get("/api/reports/summary?days=7", headers=admin).json()
    assert rep["blocked_attempts"] >= 1 and any(d["name"] == "tiktok.com" for d in rep["top_blocked_domains"])


def test_13_audit_log_hash_chain_and_immutability(client, admin):
    logs = client.get("/api/logs?limit=50", headers=admin).json()
    assert any(l["action"] == "command.restart" for l in logs)
    assert any(l["action"] == "auth.login" and l["status"] == "failed" for l in logs)
    assert client.get("/api/logs/verify", headers=admin).json()["ok"] is True
    # tamper -> terdeteksi
    from app.database import SessionLocal
    with SessionLocal() as db:
        row = db.query(ActivityLog).filter(ActivityLog.action == "command.restart").first()
        row.status = "success-palsu"
        db.commit()
    v = client.get("/api/logs/verify", headers=admin).json()
    assert v["ok"] is False
    with SessionLocal() as db:
        row = db.query(ActivityLog).filter(ActivityLog.action == "command.restart").first()
        row.status = "success"
        db.commit()
    assert client.get("/api/logs/verify", headers=admin).json()["ok"] is True
    assert client.delete("/api/logs/1", headers=admin).status_code in (404, 405)  # tidak ada endpoint hapus per baris


def test_14_rbac_admin_teacher(client, admin, enroll_key):
    cls = client.post("/api/classrooms", headers=admin, json={"name": "X-A"}).json()
    cls2 = client.post("/api/classrooms", headers=admin, json={"name": "X-B"}).json()
    t = client.post("/api/users/guru", headers=admin, json={"username": "bu.sari", "full_name": "Bu Sari", "password": "Guru12345",
                                                              "permissions": {"power": False, "lock": True, "message": True, "screen": True, "internet": True,
                                                                              "browser": True, "downloads": True, "apps": False}}).json()
    client.put(f"/api/classrooms/{cls['id']}", headers=admin, json={"name": "X-A", "teacher_id": t["id"]})
    a = register_agent(client, enroll_key, "aaaaaaaa-0010", "PC-A")
    b = register_agent(client, enroll_key, "aaaaaaaa-0011", "PC-B")
    client.patch(f"/api/computers/{a['computer_id']}", headers=admin, json={"set_classroom": True, "classroom_id": cls["id"]})
    client.patch(f"/api/computers/{b['computer_id']}", headers=admin, json={"set_classroom": True, "classroom_id": cls2["id"]})
    tok = H(client.post("/api/auth/login", json={"username": "bu.sari", "password": "Guru12345"}).json()["access_token"])
    mine = client.get("/api/computers", headers=tok).json()
    assert {c["hostname"] for c in mine} == {"PC-A"}  # hanya kelas sendiri
    assert client.get(f"/api/computers/{b['computer_id']}", headers=tok).status_code == 404
    assert client.post("/api/commands", headers=tok, json={"computer_ids": [b["computer_id"]], "action": "lock"}).status_code == 403
    assert client.post("/api/commands", headers=tok, json={"computer_ids": [a["computer_id"]], "action": "shutdown", "confirm": True}).status_code == 403  # izin power dimatikan
    assert client.post("/api/commands", headers=tok, json={"computer_ids": [a["computer_id"]], "action": "lock"}).status_code == 200
    # guru tidak bisa kelola pengguna / pengaturan / hapus log / tugaskan ke semua
    assert client.get("/api/users", headers=tok).status_code == 403
    assert client.put("/api/settings", headers=tok, json={"values": {"school_name": "x"}}).status_code == 403
    assert client.post("/api/logs/purge?days=30&confirm=true", headers=tok).status_code == 403
    pid = client.get("/api/policies/internet", headers=tok).json()[0]["id"]
    assert client.post("/api/assignments", headers=tok, json={"policy_type": "internet", "policy_id": pid, "target_type": "all"}).status_code == 403
    assert client.post("/api/assignments", headers=tok, json={"policy_type": "internet", "policy_id": pid, "target_type": "classroom", "target_id": cls["id"]}).status_code == 200
    assert client.post("/api/assignments", headers=tok, json={"policy_type": "internet", "policy_id": pid, "target_type": "classroom", "target_id": cls2["id"]}).status_code == 403
    assert client.get("/api/app-requests", headers=tok).status_code == 403  # izin apps dimatikan
    # tanpa token
    assert client.get("/api/computers").status_code == 401
    assert client.get("/api/computers", headers=H("token.palsu.sekali")).status_code == 401
    # log guru hanya komputer kelasnya
    assert all(l["computer_id"] in (None, a["computer_id"]) or False for l in client.get("/api/logs", headers=tok).json() if l["computer_id"])


def test_15_schedule_usage_lock_and_wol(client, admin, enroll_key):
    from datetime import datetime
    from app.services.policy import effective_policy
    from app.database import SessionLocal
    from app.models import Computer
    from app.services.wol import magic_packet
    info = register_agent(client, enroll_key, "aaaaaaaa-0012", "PC-12")
    cid = info["computer_id"]
    now = datetime.now()
    other_day = [(now.weekday() + 1) % 7]
    s = client.post("/api/schedules", headers=admin, json={"name": "Jam Lab", "kind": "usage", "computer_id": cid, "days": other_day, "start_time": "07:00", "end_time": "15:00"})
    assert s.status_code == 200, s.text
    with SessionLocal() as db:
        pol = effective_policy(db, db.get(Computer, cid), now)
        assert pol["lock"]["locked"] is True and "jadwal" in pol["lock"]["message"].lower()
        pol2 = effective_policy(db, db.get(Computer, cid), datetime(now.year, now.month, now.day, 8, 0))  # hari ini jam 08:00? bergantung hari
    client.delete(f"/api/schedules/{s.json()['id']}", headers=admin)
    with SessionLocal() as db:
        assert effective_policy(db, db.get(Computer, cid), now)["lock"]["locked"] is False
    assert magic_packet("AA:BB:CC:DD:EE:01")[:6] == b"\xff" * 6 and len(magic_packet("AA-BB-CC-DD-EE-01")) == 102
    r = client.post("/api/commands", headers=admin, json={"computer_ids": [cid], "action": "wake"}).json()["results"][0]
    assert r["status"] in ("success", "failed")  # tergantung izin socket sandbox


def test_16_rate_limit_and_security_headers(client):
    r = client.get("/api/health")
    assert r.headers["x-content-type-options"] == "nosniff" and r.headers["x-frame-options"] == "DENY"
    from app.middleware.ratelimit import RateLimitMiddleware
    from fastapi import FastAPI
    from fastapi.testclient import TestClient
    mini = FastAPI()
    mini.add_middleware(RateLimitMiddleware, rules={"/api/x": 3})

    @mini.get("/api/x")
    def x():
        return {}
    c = TestClient(mini)
    codes = [c.get("/api/x").status_code for _ in range(5)]
    assert codes == [200, 200, 200, 429, 429]


def test_17_sql_injection_is_inert(client, admin):
    r = client.get("/api/students", headers=admin, params={"q": "' OR 1=1; DROP TABLE students;--"})
    assert r.status_code == 200 and r.json() == []
    r = client.post("/api/auth/login", json={"username": "admin' --", "password": "x"})
    assert r.status_code == 401
    assert client.get("/api/students", headers=admin).status_code == 200
