"""A Host is refused by the API, not merely shown fewer buttons (task 8.8).

Replaces `test_viewer_permissions.py`. That file asked whether a read-only role was refused
every write; there is no read-only role now, and a Host may make all of those writes — inside
the events they own. So the question this file asks is the one that survived the collapse to
two roles: **are the three system-wide capabilities refused by the API itself?**

The frontend's `can()` hides controls, and it would be easy to mistake a hidden button for an
enforced rule. It is not one: anyone holding a Host session cookie can issue these requests by
hand.

The other half of a Host's boundary is scope — which records, not which kinds of action — and
that lives in `test_access_control_integration.py`. Both have to hold; either alone lets a
Host somewhere they should not be.
"""

import uuid
from collections.abc import Iterator
from typing import Any

import pytest
from httpx import ASGITransport, AsyncClient

from app.main import app
from app.models.enums import AdminRole, AuthMethod
from app.services.auth import CurrentAdmin, get_current_admin
from app.services.policy import Action, can

from .conftest import requires_db

pytestmark = [requires_db, pytest.mark.usefixtures("app_engine_per_loop")]

#: Every request a Host must be refused on capability grounds alone — no scope involved,
#: because none of these belongs to an event. Path, method, and a body where the route needs
#: one to get past validation: a 422 would prove nothing about authorization.
FORBIDDEN: list[tuple[str, str, dict[str, Any] | None]] = [
    ("GET", "/api/admin/users", None),
    ("POST", "/api/admin/users", {"email": "someone@example.com", "auth_method": "google"}),
    ("PATCH", "/api/admin/users/{admin_id}", {"role": "super_admin"}),
    ("POST", "/api/admin/users/{admin_id}/activate", {"role": "super_admin"}),
    ("POST", "/api/admin/users/{admin_id}/withdraw", None),
    ("POST", "/api/admin/users/{admin_id}/temporary-password", None),
    ("DELETE", "/api/admin/users/{admin_id}", None),
    ("PATCH", "/api/admin/wedding", {"host_email": "hijack@example.com"}),
    ("PUT", "/api/admin/wedding/invitation-messages", {"messages": {}}),
]  # fmt: skip


@pytest.fixture
def as_host() -> Iterator[None]:
    """A Host session, minted the same way the real guard would see one."""
    app.dependency_overrides[get_current_admin] = lambda: CurrentAdmin(
        id=uuid.uuid4(),
        email="host@example.com",
        name="Host",
        role=AdminRole.HOST,
        auth_method=AuthMethod.GOOGLE,
    )
    yield
    app.dependency_overrides.pop(get_current_admin, None)


def test_the_matrix_agrees_that_a_host_holds_none_of_these() -> None:
    """Guards the premise of every test below.

    If the matrix ever granted a Host one of these, the 403 assertions would start failing
    for the right reason, and this names which one immediately.
    """
    assert not can(AdminRole.HOST, Action.MANAGE_ADMINS)
    assert not can(AdminRole.HOST, Action.VIEW_AUDIT_LOG)
    assert not can(AdminRole.HOST, Action.CONFIGURE_GATEWAYS)


def test_the_matrix_agrees_that_a_host_runs_their_own_events() -> None:
    """The other half, so this file cannot be read as "a Host may do nothing"."""
    assert can(AdminRole.HOST, Action.EDIT_CONTENT)
    assert can(AdminRole.HOST, Action.ADD_EDIT_GUESTS)
    assert can(AdminRole.HOST, Action.DELETE_GUESTS)
    assert can(AdminRole.HOST, Action.SEND_MESSAGES)


@pytest.mark.usefixtures("as_host")
@pytest.mark.parametrize("method,path,body", FORBIDDEN, ids=lambda v: str(v)[:40])
@pytest.mark.asyncio
async def test_a_host_is_refused_by_the_api(
    method: str, path: str, body: dict[str, Any] | None
) -> None:
    url = path.format(admin_id=uuid.uuid4())
    async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
        response = await client.request(method, url, json=body)

    assert response.status_code == 403, (
        f"{method} {url} answered {response.status_code}, not 403 — "
        "a Host reached a system-wide surface"
    )


@pytest.mark.usefixtures("as_host")
@pytest.mark.asyncio
async def test_a_host_can_still_read_their_own_dashboard() -> None:
    """The control. Without it, a bug that refuses a Host everything would pass this file."""
    async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
        response = await client.get("/api/admin/stats")
    assert response.status_code == 200


@pytest.mark.usefixtures("as_host")
@pytest.mark.asyncio
async def test_the_roster_refusal_is_403_not_404() -> None:
    """Deliberately different from a scope refusal (design D3).

    404 is for records whose existence must not be confirmed. The roster plainly exists and
    the caller's own account is on it, so hiding it would be theatre — the honest answer is
    that they are authenticated and not permitted.
    """
    async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
        response = await client.get("/api/admin/users")
    assert response.status_code == 403
