"""Uji unit Client Agent: penegakan kebijakan (mode dry-run mencatat persis apa yang akan dijalankan di Windows)."""
import ipaddress
import json
import time
import xml.etree.ElementTree as ET

import pytest

from modules.policy_engine import (HOSTS, PolicyEngine, blocked_ranges_v4, build_applocker_xml, url_verdict)
from modules.watcher import classify
from modules.winops import Ops
from services.agent_core import canonical, verify_command
import hashlib
import hmac

POL_BLOCKLIST = {"mode": "blocklist", "allow_domains": ["classroom.google.com", "youtube.com"], "block_domains": ["tiktok.com", "youtube.com", "vpnbook.com"]}
POL_ALLOWLIST = {"mode": "allowlist", "allow_domains": ["classroom.google.com", "wikipedia.org"], "block_domains": ["vpnbook.com"]}


def test_url_verdict_blocklist_allowlist_and_specificity():
    assert url_verdict("https://www.tiktok.com/@x", POL_BLOCKLIST)[0] is False
    assert url_verdict("https://m.tiktok.com", POL_BLOCKLIST)[0] is False
    assert url_verdict("https://nottiktok.com", POL_BLOCKLIST)[0] is True  # bukan subdomain sungguhan
    assert url_verdict("https://youtube.com/watch", POL_BLOCKLIST)[0] is True  # whitelist = sama panjang -> allow? (lihat bawah)
    assert url_verdict("https://google.com", POL_BLOCKLIST)[0] is True
    assert url_verdict("https://vpnbook.com", POL_BLOCKLIST)[0] is False
    assert url_verdict("https://classroom.google.com/c/1", POL_ALLOWLIST)[0] is True
    assert url_verdict("https://id.wikipedia.org/wiki/X", POL_ALLOWLIST)[0] is True
    assert url_verdict("https://facebook.com", POL_ALLOWLIST)[0] is False
    assert url_verdict("https://vpnbook.com", POL_ALLOWLIST)[0] is False
    assert url_verdict("https://example.com", {"mode": "block_all"})[0] is False
    assert url_verdict("https://example.com", {"mode": "allow_all"})[0] is True
    assert url_verdict("http://localhost:8443/x", {"mode": "block_all"})[0] is True
    assert url_verdict("chrome://settings", {"mode": "block_all"})[0] is True


def test_firewall_ranges_exclude_lan_and_server():
    ranges = blocked_ranges_v4("203.0.113.7")
    nets = [ipaddress.ip_network(r) for r in ranges]
    for ok in ("192.168.1.5", "10.1.2.3", "172.20.0.9", "127.0.0.1", "203.0.113.7", "169.254.1.1"):
        assert not any(ipaddress.ip_address(ok) in n for n in nets), ok
    for blocked in ("8.8.8.8", "142.250.1.1", "203.0.113.8", "1.1.1.1", "172.32.0.1", "11.0.0.1"):
        assert any(ipaddress.ip_address(blocked) in n for n in nets), blocked
    assert len(ranges) < 150
    ops = Ops(dry_run=True)
    PolicyEngine(ops, server_ip="203.0.113.7").firewall({"mode": "block_all"})
    rules = [c[-1] for c in ops.commands]
    import re
    for chunk in re.findall(r"-RemoteAddress ([0-9./,]+)", " ".join(rules)):
        assert len(chunk.split(",")) <= 40  # dipecah supaya tidak melebihi batas Windows Firewall


def _policy(mode="blocklist", **over):
    p = {"internet": {"mode": mode, "allow_domains": ["classroom.google.com"], "block_domains": ["tiktok.com", "vpnbook.com"]},
         "browser": {"max_tabs": 3, "allowed_browsers": ["chrome", "edge"], "homepage": "https://classroom.google.com", "block_incognito": True,
                     "downloads_require_approval": True, "exam_mode": False},
         "applications": {"mode": "blocklist", "blocked_apps": ["utorrent.exe"], "allowed_apps": [], "block_store": True, "block_installers": True},
         "lock": {"locked": False, "message": ""}, "version": "t1"}
    p.update(over)
    return p


def test_engine_blocklist_writes_hosts_and_chrome_policies():
    ops = Ops(dry_run=True)
    ops.files[HOSTS] = "127.0.0.1 localhost\n"
    eng = PolicyEngine(ops, server_ip="192.168.1.2")
    assert eng.apply(_policy()) == []
    hosts = ops.files[HOSTS]
    assert "127.0.0.1 localhost" in hosts and "0.0.0.0 tiktok.com" in hosts and "0.0.0.0 www.tiktok.com" in hosts
    c = r"SOFTWARE\Policies\Google\Chrome"
    assert ops.registry[(c + r"\URLBlocklist", "1")] in ("tiktok.com", "vpnbook.com")
    assert ops.registry[(c + r"\URLAllowlist", "1")] == "classroom.google.com"
    assert ops.registry[(c, "DnsOverHttpsMode")] == "off"
    assert ops.registry[(c, "IncognitoModeAvailability")] == 1
    assert ops.registry[(r"SOFTWARE\Policies\Microsoft\Edge", "InPrivateModeAvailability")] == 1
    assert ops.registry[(c, "DeveloperToolsAvailability")] == 2
    assert ops.registry[(c, "HomepageLocation")] == "https://classroom.google.com"
    assert ops.registry[(r"SOFTWARE\Policies\Microsoft\WindowsStore", "RemoveWindowsStore")] == 1
    fw = [" ".join(cmd) for cmd in ops.commands if "Remove-NetFirewallRule" in " ".join(cmd)]
    assert fw and not any("New-NetFirewallRule" in " ".join(cmd) for cmd in ops.commands)  # blocklist -> tidak memakai firewall


def test_engine_block_all_creates_firewall_rules_and_idempotent_hosts():
    ops = Ops(dry_run=True)
    ops.files[HOSTS] = "127.0.0.1 localhost\n"
    eng = PolicyEngine(ops, server_ip="192.168.1.2")
    eng.apply(_policy("block_all"))
    ps = " ".join(" ".join(c) for c in ops.commands)
    assert "New-NetFirewallRule" in ps and "-Action Block" in ps and "-Direction Outbound" in ps and "2000::/3" in ps
    assert ops.registry[(r"SOFTWARE\Policies\Google\Chrome\URLBlocklist", "1")] == "*"
    for _ in range(3):
        eng.apply(_policy("blocklist"))
    h = ops.files[HOSTS]
    assert h.count("# BEGIN CMS-AGENT") == 1 and h.count("0.0.0.0 tiktok.com") == 1  # tidak menumpuk
    eng.apply(_policy("allow_all", internet={"mode": "allow_all", "allow_domains": [], "block_domains": []}))
    assert "CMS-AGENT" not in ops.files[HOSTS] and "127.0.0.1 localhost" in ops.files[HOSTS]  # bersih saat izinkan semua
    assert (r"SOFTWARE\Policies\Google\Chrome\URLBlocklist", "1") not in ops.registry


def test_engine_allowlist_mode_blocks_wildcard_with_exceptions():
    ops = Ops(dry_run=True)
    PolicyEngine(ops).apply(_policy("allowlist"))
    c = r"SOFTWARE\Policies\Microsoft\Edge"
    assert ops.registry[(c + r"\URLBlocklist", "1")] == "*" and ops.registry[(c + r"\URLAllowlist", "1")] == "classroom.google.com"


def test_clear_all_removes_everything():
    ops = Ops(dry_run=True)
    ops.files[HOSTS] = "127.0.0.1 localhost\n"
    eng = PolicyEngine(ops)
    eng.apply(_policy())
    eng.clear_all()
    assert "CMS-AGENT" not in ops.files[HOSTS]
    assert not [k for k in ops.registry if k[0].startswith("SOFTWARE\\Policies")]


def test_applocker_xml_valid_and_installer_rules():
    x = build_applocker_xml({"blocked_apps": ["utorrent.exe", 'a"b.exe'], "block_installers": True}, permit_active=False)
    root = ET.fromstring(x)  # XML valid walau ada karakter berbahaya
    paths = [e.get("Path") for e in root.iter("FilePathCondition")]
    assert any(p.endswith("utorrent.exe") for p in paths) and any("Downloads" in p for p in paths)
    x2 = build_applocker_xml({"blocked_apps": [], "block_installers": True}, permit_active=True)
    assert not any("Downloads" in (e.get("Path") or "") for e in ET.fromstring(x2).iter("FilePathCondition"))  # izin instal aktif


def _sig(key, cid=7, action="lock", params=None, exp=None, nonce="n1"):
    payload = {"id": cid, "action": action, "params": params or {}, "nonce": nonce, "exp": exp or int(time.time()) + 60}
    return {**payload, "sig": hmac.new(key.encode(), canonical(payload), hashlib.sha256).hexdigest()}


def test_command_verification_signature_expiry_replay():
    seen = {}
    good = _sig("kunci-rahasia")
    assert verify_command(good, "kunci-rahasia", seen) == (True, "ok")
    assert verify_command(good, "kunci-rahasia", seen) == (False, "perintah diputar ulang (replay)")
    forged = _sig("kunci-lain", nonce="n2")
    assert verify_command(forged, "kunci-rahasia", seen)[1] == "tanda tangan tidak valid"
    tampered = {**_sig("kunci-rahasia", nonce="n3"), "action": "shutdown"}
    assert verify_command(tampered, "kunci-rahasia", seen)[1] == "tanda tangan tidak valid"
    old = _sig("kunci-rahasia", nonce="n4", exp=int(time.time()) - 5)
    assert verify_command(old, "kunci-rahasia", seen)[1] == "perintah kedaluwarsa"
    assert verify_command({"action": "x"}, "k", seen)[0] is False


def test_server_and_agent_signatures_are_compatible():
    """Tanda tangan dari backend harus lolos verifikasi agent (kanonikalisasi identik)."""
    from app.security import build_signed_command, device_signing_key
    cmd = build_signed_command("dev-uid-0001", 5, "message", {"text": "Halo ünïcode", "seconds": 10}, 60)
    assert verify_command(cmd, device_signing_key("dev-uid-0001"), {}) == (True, "ok")


def _proc(name, exe="", cmdline=None, user="PC\\murid", create=None, pid=100):
    return {"name": name, "pid": pid, "exe": exe, "cmdline": cmdline or [exe or name], "create_time": create or time.time(), "username": user}


def test_watcher_classification():
    pol = _policy()
    ports = {"chrome": 9333, "edge": 9334}
    managed = ("c:\\programdata\\classroomagent\\profiles\\chrome",)
    t0 = time.time() - 100
    c = lambda p, pol=pol, permit=False: classify(p, pol, ports, permit, t0, managed)
    assert c(_proc("utorrent.exe", r"C:\Program Files\uTorrent\utorrent.exe")).kind == "kill"
    assert c(_proc("notepad.exe", r"C:\Windows\notepad.exe")) is None
    assert c(_proc("firefox.exe", r"C:\Program Files\Mozilla Firefox\firefox.exe")).kind == "kill"  # browser tak diizinkan
    unmanaged = c(_proc("chrome.exe", r"C:\Program Files\Google\Chrome\Application\chrome.exe"))
    assert unmanaged.kind == "launch_managed" and unmanaged.detail == "chrome"
    ok_cmd = [r"C:\chrome.exe", "--remote-debugging-port=9333", r"--user-data-dir=C:\ProgramData\ClassroomAgent\profiles\chrome"]
    assert c(_proc("chrome.exe", r"C:\chrome.exe", ok_cmd)) is None
    assert c(_proc("chrome.exe", r"C:\chrome.exe", [r"C:\chrome.exe", "--type=renderer"])) is None  # proses anak
    bad = c(_proc("chrome.exe", r"C:\chrome.exe", ok_cmd + ["--proxy-server=x:1"]))
    assert bad.kind == "kill"
    inst = c(_proc("python-3.13-setup.exe", r"C:\Users\murid\Downloads\python-3.13-setup.exe"))
    assert inst.kind == "request_install" and "python" in inst.detail
    assert c(_proc("python-3.13-setup.exe", r"C:\Users\murid\Downloads\python-3.13-setup.exe"), permit=True) is None  # izin aktif
    assert c(_proc("setup.exe", r"C:\Users\murid\Downloads\setup.exe", create=t0 - 50)) is None  # proses lama, bukan instalasi baru
    assert c(_proc("msiexec.exe", r"C:\Windows\System32\msiexec.exe", ["msiexec.exe", "/i", r"C:\x\app.msi"])).kind == "request_install"
    assert c(_proc("WinStore.App.exe", r"C:\Program Files\WindowsApps\x\WinStore.App.exe")).kind == "request_store"
    allow = _policy(applications={"mode": "allowlist", "allowed_apps": ["geogebra.exe"], "blocked_apps": [], "block_store": False, "block_installers": False})
    assert c(_proc("game.exe", r"C:\Users\murid\AppData\Local\game.exe"), allow).kind == "kill"
    assert c(_proc("geogebra.exe", r"C:\Users\murid\AppData\Local\geogebra.exe"), allow) is None
    assert c(_proc("explorer.exe", r"C:\Windows\explorer.exe")) is None  # proses sistem dilindungi
