"""Every route, mount, page and SPA path is in exactly one class of the access policy.

`services/access.py` is both the middleware's policy and this inventory: a route in no
class is refused before its handler runs, and fails here so it cannot ship unnoticed. The
walk covers every `APIRoute` for every method, every mount, the page routes (the site
mounted over a temporary shell so they exist) and every `<Route path>` in `App.tsx`; the
PUBLIC_WHEN_OPEN set must equal `tests/fixtures/open_routes.json`, so opening a route to the
public is a reviewed diff; every public write carries a reason; every permission string is
one the kit enforces. No network, no database.

The walk itself lives in `app/services/route_walk.py`, because `scripts/route-sweep.py`
reads the same list over HTTP and a script importing a test file is a second home.
"""

import json
import pathlib
import re

import pytest

from app import main
from app.services import access
from app.services.route_walk import every_key, spa_paths
from app.vendor import bw_accounts as bwa

MAIN = pathlib.Path(__file__).resolve().parents[1]
SHELL = "<!doctype html><html><head><title>t</title></head><body><div id=\"root\"></div></body></html>"


@pytest.fixture
def site(tmp_path):
    """The page routes mounted on the real app for one test, then removed again."""
    (tmp_path / "assets").mkdir()
    (tmp_path / "fonts").mkdir()
    (tmp_path / "medals").mkdir()
    (tmp_path / "index.html").write_text(SHELL)
    (tmp_path / "logo.png").write_bytes(b"png")
    before = len(main.app.routes)
    main.mount_site(main.app, tmp_path)
    try:
        yield main.app
    finally:
        del main.app.routes[before:]
        access.ROOT_FILES = frozenset()


def classes_of(key: str) -> list[str]:
    out = []
    if key in access.PUBLIC_ALWAYS:
        out.append("public_always")
    if key in access.PUBLIC_WHEN_OPEN:
        out.append("public_when_open")
    if key in access.MEMBER:
        out.append("member")
    if key in access.PERMISSION:
        out.append("permission")
    return out


def test_every_route_and_mount_is_in_exactly_one_class(site):
    keys = every_key(site)
    assert len(keys) > 80, "the walker stopped seeing included routers"
    problems = []
    for key in sorted(keys):
        cls, _ = access.classify(key, "/")
        n = len(classes_of(key))
        if cls is None or n != 1:
            problems.append(f"{key}: {n} class(es)")
    assert not problems, "classify each in exactly one class of services/access.py:\n  " + "\n  ".join(problems)


def test_every_spa_path_is_classified_deliberately():
    known = (set(access.SPA_PUBLIC_ALWAYS) | set(access.SPA_PUBLIC_WHEN_OPEN)
             | set(access.SPA_MEMBER) | set(access.SPA_PERMISSION))
    missing = []
    for path in sorted(spa_paths()):
        probe = re.sub(r":\w+", "example", path)
        if probe in known or probe.startswith(access.SPA_PUBLIC_WHEN_OPEN_PREFIXES):
            continue
        missing.append(path)
    assert not missing, f"App.tsx routes not named in services/access.py: {missing}"
    # ...and nothing is named twice.
    sets = [set(access.SPA_PUBLIC_ALWAYS), set(access.SPA_PUBLIC_WHEN_OPEN), set(access.SPA_MEMBER), set(access.SPA_PERMISSION)]
    for i, a in enumerate(sets):
        for b in sets[i + 1:]:
            assert not (a & b), a & b


def test_public_when_open_matches_the_reviewed_fixture():
    fixture = json.loads((MAIN / "tests" / "fixtures" / "open_routes.json").read_text())
    assert sorted(access.PUBLIC_WHEN_OPEN) == fixture["routes"]
    assert sorted(access.SPA_PUBLIC_WHEN_OPEN) == fixture["spa_paths"]
    assert list(access.SPA_PUBLIC_WHEN_OPEN_PREFIXES) == fixture["spa_prefixes"]


def test_every_public_write_states_why():
    for key, reason in access.PUBLIC_ALWAYS.items():
        assert reason.strip(), key
    writes = {k for k in access.PUBLIC_WHEN_OPEN if not k.startswith(("GET ", "MOUNT "))}
    assert writes == set(access.PUBLIC_WRITES_WHEN_OPEN)
    for key, reason in access.PUBLIC_WRITES_WHEN_OPEN.items():
        assert reason.strip(), key


def test_every_permission_string_is_enforced():
    for key, perm in list(access.PERMISSION.items()) + list(access.SPA_PERMISSION.items()):
        assert perm == "owner" or bwa.is_enforced(perm), (key, perm)


def test_the_kill_switch_and_the_plan_stay_the_owners_by_absence():
    assert access.PERMISSION["POST /api/sources/{slug}/enabled"] == "sources.manage"
    assert access.SPA_PERMISSION["/plan"] == "plan.view"
    for perm in ("plan.view", "items.act", "discussion.curate", "sources.manage"):
        assert perm not in {p for spec in main.accounts.SEED_LEVELS.values() for p in spec["permissions"]}


def test_machine_files_are_storefront_reads():
    assert access.MACHINE_FILES <= access.PUBLIC_WHEN_OPEN


def test_every_permission_route_and_page_is_a_live_feature_or_a_development_surface():
    """The environment placement on top of the classes (`SITE_ROLE`, Stream AW6.2): a new
    owner route that nobody placed would otherwise ship live by default, which is the way
    round that leaks. Only the permission-gated names carry an environment; the sign-in
    machinery, the storefront reads and the member routes are the same on both hosts."""
    names = set(access.PERMISSION) | set(access.SPA_PERMISSION)
    development = access.DEVELOPMENT_ROUTES | access.DEVELOPMENT_SPA
    unplaced = sorted(names - development - access.LIVE_FEATURES)
    assert not unplaced, ("name each in DEVELOPMENT_ROUTES/DEVELOPMENT_SPA or LIVE_FEATURES "
                          "of services/access.py:\n  " + "\n  ".join(unplaced))
    stale = sorted((development | access.LIVE_FEATURES) - names)
    assert not stale, "named in an environment set but in no PERMISSION class: " + ", ".join(stale)
    assert not development & access.LIVE_FEATURES
    assert not access.DEVELOPMENT_ROUTES & access.DEVELOPMENT_SPA


def test_the_development_pages_answer_by_path_and_by_route_key():
    """The three moved addresses have a server route AND an SPA path; the rest only the
    catch-all's path. Both ways in have to be closed or the environment line is decorative."""
    for path in sorted(access.DEVELOPMENT_SPA):
        assert access.is_development(access.CATCH_ALL, path), path
    for key in ("GET /todo", "GET /structure", "GET /quote", "MOUNT /docs-static",
                "GET /{page}.html", "GET /api/plan", "GET /api/discussion"):
        assert access.is_development(key, None), key
    for key in ("GET /api/collectors", "GET /api/review/sheets", "GET /api/discussion/threads"):
        assert not access.is_development(key, None), key
    for path in ("/", "/products", "/collectors", "/review", "/account", "/login"):
        assert not access.is_development(access.CATCH_ALL, path), path
