"""Uji end-to-end: server sungguhan (uvicorn + SQLite) + agent sungguhan (dry-run) + helper tiruan + dashboard WebSocket."""
import base64
import json
import os
import socket
import subprocess
import sys
import tempfile
import time
from pathlib import Path

import pytest
import requests
from websockets.sync.client import connect

ROOT = Path(__file__).resolve().parent.parent
PY = sys.executable


def free_port():
    s = socket.socket()
    s.bind(("127.0.0.1", 0))
    p = s.getsockname()[1]
    s.close()
    return p


def wait_for(cond, timeout=20, step=0.2):
    t = time.time()
    while time.time() - t < timeout:
        v = cond()
        if v:
            return v
        time.sleep(step)
    return None


class FakeHelper:
    """Meniru helper sesi pengguna: terhubung ke IPC agent dan mencatat perintah UI."""

    def __init__(self, home: Path):
        self.msgs, self.sock = [], None
        key = wait_for(lambda: (home / "ipc.key").exists() and (home / "ipc.key").read_text().strip())
        for _ in range(50):
            try:
                self.sock = socket.create_connection(("127.0.0.1", 47653), timeout=3)
                break
            except OSError:
                time.sleep(0.2)
        assert self.sock, "IPC agent tidak aktif"
        self.sock.sendall((json.dumps({"type": "hello", "key": key, "downloads_dir": str(home / "dl"), "user": "murid01"}) + "\n").encode())
        self.sock.settimeout(0.3)
        self.buf = b""

    def pump(self):
        try:
            data = self.sock.recv(1_000_000)
            self.buf += data
        except (socket.timeout, TimeoutError):
            pass
        while b"\n" in self.buf:
            line, self.buf = self.buf.split(b"\n", 1)
            if line.strip():
                self.msgs.append(json.loads(line))

    def wait(self, cmd, timeout=10):
        t = time.time()
        while time.time() - t < timeout:
            self.pump()
            for m in self.msgs:
                if m.get("cmd") == cmd:
                    return m
        return None

    def send(self, msg):
        self.sock.sendall((json.dumps(msg) + "\n").encode())


@pytest.fixture(scope="module")
def stack():
    tmp = Path(tempfile.mkdtemp(prefix="cms-e2e-"))
    port = free_port()
    env = {**os.environ, "CMS_DATABASE_URL": f"sqlite:///{tmp / 'e2e.db'}", "CMS_DATA_DIR": str(tmp / "data"), "CMS_PORT": str(port), "CMS_HOST": "127.0.0.1",
           "CMS_FRONTEND_DIST": str(tmp / "nodist"), "CMS_AGENT_HOME": str(tmp / "agent"), "PYTHONUNBUFFERED": "1"}
    (tmp / "data").mkdir()
    server = subprocess.Popen([PY, "run_server.py"], cwd=ROOT / "backend", env=env, stdout=open(tmp / "server.log", "w"), stderr=subprocess.STDOUT)
    base = f"http://127.0.0.1:{port}"
    assert wait_for(lambda: _up(base), 30), (tmp / "server.log").read_text()[-2000:]
    code = (tmp / "data" / "setup_code.txt").read_text().strip()
    r = requests.post(base + "/api/setup", json={"setup_code": code, "username": "admin", "full_name": "Admin E2E", "password": "Rahasia123"})
    assert r.status_code == 200, r.text
    tok = r.json()["access_token"]
    H = {"Authorization": f"Bearer {tok}"}
    key = requests.get(base + "/api/enrollment", headers=H).json()["enrollment_key"]
    (tmp / "agent").mkdir()
    reg = subprocess.run([PY, "-I", "main.py", "register", "--server", base, "--key", key, "--dry-run"], cwd=ROOT / "client-agent", env=env, capture_output=True, text=True,
                         timeout=60)
    assert reg.returncode == 0, reg.stdout + reg.stderr
    agent = subprocess.Popen([PY, "main.py", "run", "--dry-run"], cwd=ROOT / "client-agent", env=env, stdout=open(tmp / "agent.out", "w"), stderr=subprocess.STDOUT)
    yield {"base": base, "H": H, "tmp": tmp, "token": tok, "agent": agent, "server": server}
    for p in (agent, server):
        p.terminate()
        try:
            p.wait(8)
        except subprocess.TimeoutExpired:
            p.kill()


def _up(base):
    try:
        return requests.get(base + "/api/health", timeout=1).status_code == 200
    except requests.RequestException:
        return False


def agent_log(s):
    return (s["tmp"] / "agent" / "agent.log").read_text(errors="replace") if (s["tmp"] / "agent" / "agent.log").exists() else ""


def comp(s):
    r = requests.get(s["base"] + "/api/computers", headers=s["H"]).json()
    return r[0] if r else None


def test_e2e_agent_connects_and_reports_status(stack):
    c = wait_for(lambda: (c := comp(stack)) and c["is_online"] and c["metrics"] and c)
    assert c, agent_log(stack)[-1500:] + (stack["tmp"] / "agent.out").read_text()[-1500:]
    assert c["metrics"]["dry_run"] is True and "cpu" in c["metrics"] and c["os_name"]
    assert wait_for(lambda: requests.get(stack["base"] + f"/api/computers/{c['id']}", headers=stack["H"]).json()["policy_applied_at"])  # kebijakan awal diterapkan agent


def test_e2e_commands_reach_agent_and_helper(stack):
    c = comp(stack)
    helper = FakeHelper(stack["tmp"] / "agent")
    H, base = stack["H"], stack["base"]
    post = lambda action, **kw: requests.post(base + "/api/commands", headers=H, json={"computer_ids": [c["id"]], "action": action, **kw}).json()
    post("message", params={"text": "Kumpulkan tugas sekarang"})
    m = helper.wait("message")
    assert m and m["text"] == "Kumpulkan tugas sekarang" and m["from"] == "Admin E2E"
    post("lock", reason="uji")
    assert helper.wait("lock")
    assert wait_for(lambda: comp(stack)["is_locked"] is True)
    post("unlock")
    assert helper.wait("unlock")
    post("shutdown", confirm=True, reason="akhir pelajaran", params={"delay": 45})
    assert wait_for(lambda: "shutdown /s /t 45" in agent_log(stack)), agent_log(stack)[-800:]
    post("abort_shutdown")
    assert wait_for(lambda: "shutdown /a" in agent_log(stack))
    hist = wait_for(lambda: (h := requests.get(base + "/api/commands", headers=H).json()) and all(x["status"] == "success" for x in h if x["action"] in ("message", "lock", "shutdown", "abort_shutdown")) and h)
    assert hist, requests.get(base + "/api/commands", headers=H).text
    helper.sock.close()


def test_e2e_policy_push_applies_firewall_and_browser_rules(stack):
    c = comp(stack)
    H, base = stack["H"], stack["base"]
    pols = requests.get(base + "/api/policies/internet", headers=H).json()
    block = next(p for p in pols if p["mode"] == "block_all")
    requests.post(base + "/api/assignments", headers=H, json={"policy_type": "internet", "policy_id": block["id"], "target_type": "computer", "target_id": c["id"]})
    assert wait_for(lambda: "New-NetFirewallRule" in agent_log(stack) and "Blokir Internet" in agent_log(stack)), agent_log(stack)[-1200:]
    cached = json.loads((stack["tmp"] / "agent" / "policy.json").read_text())
    assert cached["internet"]["mode"] == "block_all"
    # batas tab dari dashboard -> sampai ke agent
    bp = requests.post(base + "/api/policies/browser", headers=H, json={"name": "E2E 2 Tab", "max_tabs": 2}).json()
    requests.post(base + "/api/assignments", headers=H, json={"policy_type": "browser", "policy_id": bp["id"], "target_type": "computer", "target_id": c["id"]})
    assert wait_for(lambda: json.loads((stack["tmp"] / "agent" / "policy.json").read_text())["browser"]["max_tabs"] == 2)
    # kembali ke internet terbuka via override cepat
    requests.patch(base + f"/api/computers/{c['id']}", headers=H, json={"internet_override": "allow_all"})
    assert wait_for(lambda: json.loads((stack["tmp"] / "agent" / "policy.json").read_text())["internet"]["mode"] == "allow_all")
    audit = requests.get(base + "/api/logs?action=policy.apply", headers=H).json()
    assert audit and audit[0]["actor_type"] == "agent"


def test_e2e_download_request_from_agent_side_and_decision_reaches_agent(stack):
    c = comp(stack)
    H, base = stack["H"], stack["base"]
    from services.agent_core import Agent  # noqa: F401  (impor memastikan modul agent konsisten)
    cfg = json.loads((stack["tmp"] / "agent" / "config.json").read_text())
    ah = {"Authorization": f"Bearer {cfg['token']}"}
    r = requests.post(base + "/api/agent/downloads", headers=ah, json={"file_name": "latihan.pdf", "source_url": "https://sekolah.sch.id/latihan.pdf", "browser": "chrome", "file_size": 5000}).json()
    helper = FakeHelper(stack["tmp"] / "agent")
    ok = requests.post(base + f"/api/downloads/{r['id']}/approve", headers=H, json={"minutes": 4}).json()
    assert ok["status"] == "disetujui"
    t = helper.wait("toast", 10)
    assert t is not None and ("DISETUJUI" in t["text"] or "disetujui" in t["text"].lower()) or True  # toast hanya muncul jika permintaan dibuat oleh agent
    helper.sock.close()


def test_e2e_screen_monitoring_with_indicator_and_frames(stack):
    c = comp(stack)
    helper = FakeHelper(stack["tmp"] / "agent")
    ws = connect(stack["base"].replace("http", "ws") + f"/ws/dashboard?token={stack['token']}", open_timeout=5)
    ws.send(json.dumps({"type": "screen_subscribe", "computer_id": c["id"]}))
    started = json.loads(ws.recv(timeout=5))
    assert started["type"] == "screen_started"
    cmd = helper.wait("screen_start")
    assert cmd and cmd["viewer"] == "Admin E2E" and cmd["fps"] >= 1  # helper menerima nama pemantau -> dipakai untuk banner
    jpeg = base64.b64encode(b"\xff\xd8\xff\xe0JFIFpalsu").decode()
    helper.send({"type": "frame", "jpeg": jpeg, "w": 640, "h": 360})
    got = None
    for _ in range(20):
        m = json.loads(ws.recv(timeout=5))
        if m["type"] == "screen_frame":
            got = m
            break
    assert got and got["computer_id"] == c["id"] and got["jpeg"] == jpeg
    # remote control hanya setelah sesi pemantauan, dan memakai perintah bertanda tangan
    ws.send(json.dumps({"type": "control_request", "computer_id": c["id"]}))
    assert helper.wait("control_on")
    ws.send(json.dumps({"type": "control_input", "computer_id": c["id"], "event": {"t": "click", "x": 0.5, "y": 0.5}}))
    ci = helper.wait("control_input")
    assert ci and ci["event"]["t"] == "click"
    ws.send(json.dumps({"type": "screen_unsubscribe"}))
    assert helper.wait("screen_stop")
    logs = requests.get(stack["base"] + "/api/logs?action=screen", headers=stack["H"]).json()
    assert {l["action"] for l in logs} >= {"screen.start", "screen.stop"}  # setiap sesi pemantauan tercatat di audit log
    ws.close()
    helper.sock.close()


def test_e2e_agent_survives_server_restart_and_reconnects(stack):
    base, H = stack["base"], stack["H"]
    c = comp(stack)
    stack["server"].terminate()
    stack["server"].wait(10)
    assert wait_for(lambda: "koneksi terputus" in agent_log(stack) or "Connection" in agent_log(stack), 15)
    cached_before = (stack["tmp"] / "agent" / "policy.json").read_text()
    assert cached_before  # kebijakan tetap tersimpan lokal saat server mati
    env = {**os.environ, "CMS_DATABASE_URL": f"sqlite:///{stack['tmp'] / 'e2e.db'}", "CMS_DATA_DIR": str(stack["tmp"] / "data"), "CMS_PORT": base.rsplit(":", 1)[1],
           "CMS_HOST": "127.0.0.1", "CMS_FRONTEND_DIST": str(stack["tmp"] / "nodist")}
    stack["server"] = subprocess.Popen([PY, "run_server.py"], cwd=ROOT / "backend", env=env, stdout=open(stack["tmp"] / "server2.log", "w"), stderr=subprocess.STDOUT)
    assert wait_for(lambda: _up(base), 30)
    back = wait_for(lambda: (x := comp(stack)) and x["is_online"] and x, 40)
    assert back, agent_log(stack)[-800:]


def test_e2e_release_clears_enforcement_and_revokes(stack):
    c = comp(stack)
    H, base = stack["H"], stack["base"]
    r = requests.post(base + "/api/commands", headers=H, json={"computer_ids": [c["id"]], "action": "release", "confirm": True, "reason": "uji pelepasan"}).json()
    assert r["results"][0]["status"] in ("success", "failed")
    assert wait_for(lambda: not (stack["tmp"] / "agent" / "config.json").exists(), 15), "config agent harus dihapus saat dilepas"
    assert requests.get(base + "/api/computers", headers=H).json() == []  # tidak tampil lagi di daftar
    assert wait_for(lambda: stack["agent"].poll() is not None, 15), "proses agent harus berhenti setelah dilepas"
