"""Tests for the auth boundary — the only thing in this app worth testing yet.

Each test asserts a behavior (a decision, a rejection, a status code), not that
something rendered. The three properties being pinned down:

  1. Default-deny holds: /api/* is closed except an exact-path allowlist.
  2. A session cookie cannot be forged, replayed past expiry, or malformed into
     acceptance.
  3. next= cannot be turned into an open redirect.
"""

from __future__ import annotations

import time

import pytest
from fastapi.testclient import TestClient

from app.app_session import (
    PROBED_COOKIE,
    SESSION_COOKIE,
    safe_next,
    sign_session,
    verify_session,
)
from app.auth import PUBLIC_API_EXACT, _is_public
from app.main import app
from app.services.roles import (
    ROLE_NONE,
    ROLE_SUPERADMIN,
    ROLE_USER,
    is_admitted,
    is_superadmin,
)

SECRET = "test-secret-not-a-real-one"


@pytest.fixture
def client() -> TestClient:
    return TestClient(app)


# --- default-deny classification -------------------------------------------


@pytest.mark.parametrize("path", sorted(PUBLIC_API_EXACT))
def test_allowlisted_api_paths_are_public(path: str) -> None:
    assert _is_public(path)


@pytest.mark.parametrize(
    "path",
    [
        "/api/account",
        "/api/version",
        "/api/admin/invites",
        "/api/openapi.json",
        # A prefix that merely STARTS with an allowlisted path must not inherit
        # its exemption — the allowlist is exact-match for exactly this reason.
        "/api/health/secrets",
        "/api/me/tokens",
    ],
)
def test_other_api_paths_are_gated(path: str) -> None:
    assert not _is_public(path)


@pytest.mark.parametrize(
    "path", ["/", "/login", "/auth/callback", "/logout", "/assets/x.js"]
)
def test_shell_and_auth_flow_are_public(path: str) -> None:
    assert _is_public(path)


# --- the gate over HTTP -----------------------------------------------------


def test_health_is_reachable_anonymously(client: TestClient) -> None:
    resp = client.get("/api/health")
    assert resp.status_code == 200
    assert resp.json() == {"ok": True}


def test_me_reports_anonymous_instead_of_401(client: TestClient) -> None:
    """PUBLIC mode depends on this: an anonymous visitor must be able to ask
    'am I signed in?' and be told no, or the SPA can never draw its welcome
    page."""
    body = client.get("/api/me").json()
    assert body["authenticated"] is False
    assert body["admitted"] is False


def test_me_offers_the_silent_probe_once_then_stops(client: TestClient) -> None:
    """should_probe must go false once the loop-guard cookie is set — that is
    what prevents an anonymous visitor bouncing between the app and id-auth."""
    assert client.get("/api/me").json()["should_probe"] is True
    client.cookies.set(PROBED_COOKIE, "1")
    assert client.get("/api/me").json()["should_probe"] is False


def test_gated_route_401s_without_a_session(client: TestClient) -> None:
    resp = client.get("/api/account")
    assert resp.status_code == 401
    assert resp.json()["error_code"] == "AUTH_SESSION_MISSING"


def test_gated_route_401s_on_a_forged_session(client: TestClient) -> None:
    forged = sign_session("rian", "not-the-server-secret", 3600)
    client.cookies.set(SESSION_COOKIE, forged)
    assert client.get("/api/account").status_code == 401


# --- session cookie integrity ----------------------------------------------


def test_valid_session_roundtrips() -> None:
    assert verify_session(sign_session("rian", SECRET, 3600), SECRET) == "rian"


def test_expired_session_is_rejected() -> None:
    expired = sign_session("rian", SECRET, -1)
    assert verify_session(expired, SECRET) is None


def test_tampered_username_is_rejected() -> None:
    """The signature covers the username, so swapping it must fail — otherwise
    any user could become any other by editing a cookie."""
    token = sign_session("someone", SECRET, 3600)
    _, exp, sig = token.split("|")
    assert verify_session(f"rian|{exp}|{sig}", SECRET) is None


def test_extended_expiry_is_rejected() -> None:
    token = sign_session("rian", SECRET, 60)
    username, _exp, sig = token.split("|")
    far_future = int(time.time()) + 999_999
    assert verify_session(f"{username}|{far_future}|{sig}", SECRET) is None


@pytest.mark.parametrize(
    "value", ["", "rian", "rian|123", "rian|abc|deadbeef", "a|b|c|d"]
)
def test_malformed_sessions_are_rejected(value: str) -> None:
    assert verify_session(value, SECRET) is None


def test_empty_secret_never_validates() -> None:
    """A deployment with no APP_SESSION_SECRET must authenticate nobody rather
    than accept everybody."""
    assert verify_session(sign_session("rian", SECRET, 3600), "") is None


# --- open redirect ----------------------------------------------------------


@pytest.mark.parametrize(
    "value",
    ["//evil.example", "/\\evil.example", "https://evil.example", "evil", None, ""],
)
def test_safe_next_rejects_offsite_targets(value: str | None) -> None:
    assert safe_next(value) == "/"


def test_safe_next_keeps_same_origin_paths() -> None:
    assert safe_next("/library?tab=daily") == "/library?tab=daily"


# --- admission --------------------------------------------------------------


def test_only_invited_roles_are_admitted() -> None:
    assert is_admitted(ROLE_SUPERADMIN)
    assert is_admitted(ROLE_USER)
    assert not is_admitted(ROLE_NONE)


@pytest.mark.parametrize("role", ["", "admin", "owner", "None", "SUPERADMIN"])
def test_unknown_roles_are_denied(role: str) -> None:
    """Default-deny: a role the app does not recognize grants nothing. A typo in
    a hand-written database row must fail closed, not open."""
    assert not is_admitted(role)
    assert not is_superadmin(role)
