from __future__ import annotations

import http.client
import importlib
import os
import sqlite3
import sys
import threading
from pathlib import Path
from urllib.parse import quote

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts" / "health"))
sys.path.insert(0, str(ROOT / "tests"))
os.environ.setdefault("HEALTH_DASHBOARD_FILE", str(ROOT / "tests" / "synthetic.html"))
os.environ.pop("HEALTH_DASHBOARD_TEST_INSTANCE_ID", None)
os.environ.pop("HEALTH_DASHBOARD_ACTION_INBOX", None)

read_api = importlib.import_module("dashboard_v5.read_api")
dispatch_api = read_api.dispatch_api
fixtures = importlib.import_module("fixtures.dashboard_v5_fixture")
build_dashboard_v5_fixture = fixtures.build_dashboard_v5_fixture
server = importlib.import_module("health_dashboard_server")


def _fixture(tmp_path: Path) -> tuple[Path, str, Path]:
    database = tmp_path / "original.db"
    build_dashboard_v5_fixture(database)
    allowed = tmp_path / "allowed"
    allowed.mkdir()
    original = allowed / "synthetic.pdf"
    original.write_bytes(b"%PDF-1.7\nsynthetic")
    connection = sqlite3.connect(database)
    connection.execute(
        """INSERT INTO dokumente(
               datei_name,dateipfad,local_original_path,daten_typ,kategorie,
               institution,review_status,document_date,extrahierte_inhalte
           ) VALUES(?,?,?,?,?,?,?,?,?)""",
        (
            "secret-original-name.pdf",
            str(original),
            str(original),
            "pdf",
            "Synthetic",
            "Synthetic Clinic",
            "geprueft",
            "2026-06-01",
            "synthetic",
        ),
    )
    connection.commit()
    connection.close()
    opaque = dispatch_api(database, "/api/v1/documents", "category=Synthetic")[
        "documents"
    ][0]["id"]
    return database, opaque, allowed


def _update_path(database: Path, value: str, *, reviewed: str = "geprueft") -> None:
    connection = sqlite3.connect(database)
    connection.execute(
        """UPDATE dokumente
           SET local_original_path=?,dateipfad=?,review_status=?
           WHERE kategorie='Synthetic'""",
        (value, value, reviewed),
    )
    connection.commit()
    connection.close()


def test_descriptor_open_rejects_traversal_symlinks_magic_and_size(
    tmp_path, monkeypatch
):
    database, opaque, allowed = _fixture(tmp_path)
    monkeypatch.setattr(server, "BASE", allowed)
    monkeypatch.setattr(server, "REPORTS", allowed)
    opened = server.open_safe_original(database, opaque)
    assert opened and opened[2:] == ("application/pdf", "document.pdf")
    os.close(opened[0])

    _update_path(database, "synthetic.pdf")
    relative = server.open_safe_original(database, opaque)
    assert relative
    os.close(relative[0])

    outside = tmp_path / "outside.pdf"
    outside.write_bytes(b"%PDF-1.7\noutside")
    for value in (str(outside), "../outside.pdf", "%2e%2e/outside.pdf"):
        _update_path(database, value)
        assert server.open_safe_original(database, opaque) is None

    final_link = allowed / "final.pdf"
    final_link.symlink_to(outside)
    _update_path(database, str(final_link))
    assert server.open_safe_original(database, opaque) is None

    real_dir = allowed / "real"
    real_dir.mkdir()
    (real_dir / "inside.pdf").write_bytes(b"%PDF-1.7\ninside")
    middle = allowed / "middle"
    middle.symlink_to(real_dir, target_is_directory=True)
    _update_path(database, str(middle / "inside.pdf"))
    assert server.open_safe_original(database, opaque) is None

    wrong_magic = allowed / "wrong.pdf"
    wrong_magic.write_bytes(b"not-a-pdf")
    _update_path(database, str(wrong_magic))
    assert server.open_safe_original(database, opaque) is None

    directory = allowed / "directory"
    directory.mkdir()
    _update_path(database, str(directory))
    assert server.open_safe_original(database, opaque) is None

    large = allowed / "large.pdf"
    large.write_bytes(b"%PDF-" + b"x" * 64)
    monkeypatch.setattr(server, "MAX_ORIGINAL_BYTES", 16)
    _update_path(database, str(large))
    assert server.open_safe_original(database, opaque) is None
    monkeypatch.setattr(server, "MAX_ORIGINAL_BYTES", 20 * 1024 * 1024)

    fifo = allowed / "blocked.fifo"
    os.mkfifo(fifo)
    _update_path(database, str(fifo))
    assert server.open_safe_original(database, opaque) is None

    original_open = server._open_regular_beneath
    swap_target = allowed / "swap.pdf"
    swap_target.write_bytes(b"%PDF-1.7\nselected")
    _update_path(database, str(swap_target))

    def exchange_before_open(root, candidate):
        swap_target.unlink()
        swap_target.symlink_to(outside)
        return original_open(root, candidate)

    monkeypatch.setattr(server, "_open_regular_beneath", exchange_before_open)
    assert server.open_safe_original(database, opaque) is None

    monkeypatch.setattr(server, "_open_regular_beneath", original_open)
    _update_path(database, str(allowed / "synthetic.pdf"), reviewed="nicht_geprueft")
    unreviewed = server.open_safe_original(database, opaque)
    assert unreviewed is not None
    os.close(unreviewed[0])
    assert server.open_safe_original(database, "api-document-" + "0" * 24) is None


def test_original_http_auth_head_headers_and_no_leaks(tmp_path, monkeypatch):
    database, opaque, allowed = _fixture(tmp_path)
    token_file = tmp_path / "token"
    token = "s" * 48
    token_file.write_text(token, encoding="ascii")
    token_file.chmod(0o600)
    monkeypatch.setattr(server, "BASE", allowed)
    monkeypatch.setattr(server, "REPORTS", allowed)
    monkeypatch.setattr(server, "API_DB", database)
    monkeypatch.setattr(server, "API_TOKEN_FILE", token_file)
    httpd = server.ThreadingHTTPServer(("127.0.0.1", 0), server.Handler)
    thread = threading.Thread(target=httpd.serve_forever, daemon=True)
    thread.start()
    host, port = httpd.server_address

    def request(
        method: str,
        target: str,
        *,
        authenticated: bool = True,
        fetch_site: str = "same-origin",
        cookie: str | None = None,
    ):
        connection = http.client.HTTPConnection(host, port, timeout=5)
        headers = {"Host": f"127.0.0.1:{port}", "Sec-Fetch-Site": fetch_site}
        if authenticated:
            headers["Authorization"] = f"Bearer {token}"
        if cookie:
            headers["Cookie"] = cookie
        connection.request(method, target, headers=headers)
        response = connection.getresponse()
        body = response.read()
        result = response.status, dict(response.getheaders()), body
        connection.close()
        return result

    try:
        route = f"/api/v1/documents/{opaque}/original"
        status, headers, body = request("GET", route, authenticated=False)
        assert status == 401 and b"authentication_required" in body
        status, headers, body = request("GET", route, authenticated=True)
        assert status == 200 and body.startswith(b"%PDF-")
        assert headers["Cache-Control"] == "no-store"
        assert headers["X-Content-Type-Options"] == "nosniff"
        assert headers["Referrer-Policy"] == "no-referrer"
        assert headers["Content-Disposition"] == 'attachment; filename="health-document-original.pdf"'
        _update_path(database, str(allowed / "synthetic.pdf"), reviewed="nicht_geprueft")
        status, headers, body = request("HEAD", route)
        assert status == 200 and body == b""
        assert int(headers["Content-Length"]) > 0
        status, _, body = request("GET", route)
        assert status == 200 and body.startswith(b"%PDF-")
        with sqlite3.connect(database) as check:
            assert check.execute("SELECT review_status FROM dokumente WHERE kategorie='Synthetic'").fetchone()[0] == "nicht_geprueft"

        # A successful HEAD is never a capability token: GET reopens and revalidates.
        original = allowed / "synthetic.pdf"
        after_head_outside = tmp_path / "after-head.pdf"
        after_head_outside.write_bytes(b"%PDF-1.7\noutside")
        original.unlink()
        original.symlink_to(after_head_outside)
        status, _, body = request("GET", route)
        assert status == 404 and b"original_not_available" in body
        original.unlink()
        original.write_bytes(b"%PDF-1.7\nsynthetic")
        status, _, body = request(
            "GET",
            route,
            authenticated=False,
            cookie="health_v5_session=invalid-or-expired",
        )
        assert status == 401 and b"authentication_required" in body
        status, _, body = request("GET", route, fetch_site="cross-site")
        assert status == 403 and b"origin_not_allowed" in body

        stream_file = allowed / "stream.pdf"
        stream_file.write_bytes(b"%PDF-1.7\n" + b"s" * (64 * 1024 * 128))
        _update_path(database, str(stream_file))
        aborted = http.client.HTTPConnection(host, port, timeout=5)
        aborted.request(
            "GET",
            route,
            headers={
                "Host": f"127.0.0.1:{port}",
                "Sec-Fetch-Site": "same-origin",
                "Authorization": f"Bearer {token}",
            },
        )
        aborted_response = aborted.getresponse()
        assert aborted_response.status == 200
        assert aborted_response.read(1) == b"%"
        aborted.close()
        status, _, body = request("HEAD", route)
        assert status == 200 and body == b""

        for suffix in ("?x=1", "?x=1&x=2", "?document=bad"):
            status, _, body = request("GET", route + suffix)
            assert status == 400 and b"unknown_parameter" in body
        status, headers, body = request(
            "GET", f"/api/v1/documents/{quote('../secret')}/original"
        )
        assert status == 404
        serialized = repr((headers, body)).casefold()
        for secret in (
            str(allowed).casefold(),
            "secret-original-name",
            "file:",
            "drive",
        ):
            assert secret not in serialized
    finally:
        httpd.shutdown()
        httpd.server_close()
        thread.join(timeout=5)
