"""Security gate: EVERY API route requires an app session (M8 hardening;
the app half of the qBittorrent-breach lesson — no unauthenticated
surface outside an explicit, minimal allowlist).

This is a meta-test over the live route table: it enumerates every
APIRoute × method and asserts an unauthenticated request is denied
(default-deny runs BEFORE routing, so the answer is a structured 401),
except for the four exact public paths. A new router that forgets auth
cannot pass this — there is no per-router opt-in; the gate is global.
"""

import re

from fastapi.routing import APIRoute

from app.main import create_app
from app.services.auth.middleware import (
    ALLOWLIST_EXACT,
    REGISTRY_PREFIX,
    is_public_path,
)

#: The ONLY paths that may serve without a session (T-AZ-041/042).
EXPECTED_ALLOWLIST = frozenset(
    {"/healthz", "/auth/login", "/auth/callback", "/auth/logout"}
)
_METHODS = ("GET", "POST", "PATCH", "PUT", "DELETE")


def _routes(app) -> list[tuple[str, str]]:
    out: list[tuple[str, str]] = []

    def walk(routes):
        for route in routes:
            if isinstance(route, APIRoute):
                for method in route.methods or ():
                    if method in _METHODS:
                        out.append((route.path, method))
            original = getattr(route, "original_router", None)
            if original is not None:
                walk(original.routes)

    walk(app.routes)
    return out


def _concrete(path: str) -> str:
    """Fill path params with a dummy uuid so the URL is well-formed.
    (Default-deny answers before routing, so the value is irrelevant —
    but a real URL keeps the test honest.)"""
    return re.sub(r"\{[^}]+\}", "00000000-0000-0000-0000-000000000000", path)


def test_allowlist_is_exactly_the_public_set():
    assert ALLOWLIST_EXACT == EXPECTED_ALLOWLIST
    # nothing under /api may ever be public
    assert not any(p.startswith("/api/") for p in ALLOWLIST_EXACT)
    assert not is_public_path("/api/meta")


def test_every_api_route_requires_a_session(client):
    app = create_app()
    checked = registry_checked = 0
    for path, method in _routes(app):
        if path in ALLOWLIST_EXACT:
            continue
        assert path.startswith("/api/") or path.startswith("/auth/"), (
            f"unexpected non-API route {method} {path} — add it to the "
            "allowlist review or make it /api/*"
        )
        if path.startswith("/auth/"):
            continue  # the OAuth flow routes are the allowlist's job
        if path.startswith(REGISTRY_PREFIX):
            # Registry routes are server-to-server (ADR #014): they carry NO
            # app session and are deliberately bypassed by the default-deny
            # SESSION gate — they enforce their OWN service-token auth at the
            # router layer instead. The "no unauthenticated surface"
            # invariant still holds; it's just the token, not the cookie. In
            # the test env no token is configured, so an unauthenticated
            # caller fails CLOSED with 503 REGISTRY_DISABLED (a wrong/absent
            # token would be 401). Either way: denied, never 200, never 500.
            resp = client.request(method, _concrete(path), follow_redirects=False)
            assert resp.status_code in (401, 503), (
                f"{method} {path} was not token-gated: {resp.status_code}"
            )
            assert resp.json()["error_code"] in (
                "REGISTRY_DISABLED",
                "REGISTRY_AUTH_REQUIRED",
                "REGISTRY_AUTH_INVALID",
            ), (path, resp.json())
            registry_checked += 1
            continue
        resp = client.request(method, _concrete(path), follow_redirects=False)
        assert resp.status_code == 401, f"{method} {path} did not 401 unauthenticated"
        body = resp.json()
        assert body["error_code"] == "NOT_AUTHENTICATED", (path, body)
        checked += 1
    assert checked > 15, f"route walk only checked {checked} — walker broken?"
    assert registry_checked >= 1, "registry routes not found — walker or wiring broken?"
