from __future__ import annotations

import json
import socket
import time
from urllib.error import HTTPError
from urllib.request import urlopen

from auditwalk.gui.observer_server import ObserverServer


def _free_port() -> int:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.bind(("127.0.0.1", 0))
        return int(s.getsockname()[1])


def _base_status() -> dict:
    return {
        "mode": "observer-read-only",
        "state": "RUNNING",
        "platform": "linux",
        "scan": {
            "phase": "scanning",
            "progress_percent": 42,
            "files_scanned": 21,
            "files_total": 50,
            "throughput_files_per_sec": 12.3,
        },
        "findings": {
            "state": "CLEAR",
            "summary": "none",
            "drift": "none",
            "total": 0,
            "severity_counts": {"critical": 0, "high": 0, "medium": 0, "low": 0},
        },
        "preflight": {"summary": "pass"},
        "last_trusted_state": "2026-03-02T10:00:00",
        "channel": "Local Secure Session",
    }


def test_observer_server_pair_and_status_token_gate() -> None:
    port = _free_port()
    server = ObserverServer(status_provider=_base_status, host="127.0.0.1", port=port, token_ttl_seconds=300)
    server.start()
    try:
        with urlopen(f"http://127.0.0.1:{port}/pair") as resp:
            payload = json.loads(resp.read().decode("utf-8"))
        token = payload["token"]

        with urlopen(f"http://127.0.0.1:{port}/observer/status?token={token}") as resp:
            status = json.loads(resp.read().decode("utf-8"))
        assert status["scan"]["phase"] == "scanning"
        assert "session_id" in status

        try:
            urlopen(f"http://127.0.0.1:{port}/observer/status?token=bad")
            assert False, "expected 401 for invalid token"
        except HTTPError as exc:
            assert exc.code == 401
    finally:
        server.stop()


def test_observer_html_security_headers_and_token_render() -> None:
    port = _free_port()
    server = ObserverServer(status_provider=_base_status, host="127.0.0.1", port=port, token_ttl_seconds=300)
    server.start()
    try:
        with urlopen(f"http://127.0.0.1:{port}/pair") as resp:
            pair = json.loads(resp.read().decode("utf-8"))
        token = pair["token"]

        with urlopen(f"http://127.0.0.1:{port}/observer?token={token}") as resp:
            html = resp.read().decode("utf-8")
            headers = resp.headers

        csp = headers.get("Content-Security-Policy", "")
        assert "default-src 'self'" in csp
        assert headers.get("Referrer-Policy") == "no-referrer"
        assert headers.get("X-Content-Type-Options") == "nosniff"
        assert headers.get("Content-Type", "").startswith("text/html")
        assert "window.__OBSERVER_TOKEN__ = " in html
        assert token in html
        assert "/observer/assets/observer.css" in html
        assert "/observer/assets/observer.css?token=" not in html
        assert "/observer/assets/observer.js?token=" not in html
    finally:
        server.stop()


def test_observer_assets_and_diagnostics_without_asset_token() -> None:
    port = _free_port()
    server = ObserverServer(status_provider=_base_status, host="127.0.0.1", port=port, token_ttl_seconds=300)
    server.start()
    try:
        with urlopen(f"http://127.0.0.1:{port}/observer/assets/observer.css") as resp:
            css = resp.read().decode("utf-8")
            assert "isolation: isolate" in css
            assert resp.headers.get("X-Content-Type-Options") == "nosniff"

        with urlopen(f"http://127.0.0.1:{port}/pair") as resp:
            pair = json.loads(resp.read().decode("utf-8"))
        token = pair["token"]

        with urlopen(f"http://127.0.0.1:{port}/observer/diagnostics?token={token}") as resp:
            diag = json.loads(resp.read().decode("utf-8"))
        assert diag["support_ticket_url"].startswith("https://support.auditwalk.com/tickets/new?")
        assert "session_id=" in diag["support_ticket_url"]
    finally:
        server.stop()


# guard against hanging event stream tests in slow CI; no stream assertions needed for this pass
