"""Тесты API системных уведомлений (ТЗ 3.21): config, подписки, изоляция.

По умолчанию ключей VAPID нет — push выключен, и это первое, что проверяем: без
ключей подписываться некуда, API обязан сказать об этом честно, а не падать на
отправке позже. Ключи в тестах включаются точечно (`vapid_keys`), причём фикстура
ниже глушит и те, что лежат в `.env` разработчика: набор не должен зависеть от
того, завёл ли он себе push.
"""

from collections.abc import Iterator
from contextlib import contextmanager
from typing import Any

import pytest
from fastapi.testclient import TestClient
from sqlalchemy import select

from app.config import get_settings
from app.dependencies import require_user
from app.main import app
from app.models import PushSubscription
from tests.conftest import _test_session_factory  # type: ignore[attr-defined]
from tests.test_multiuser import switch_user

ENDPOINT = "https://push.example.com/sub/abc123"
PAYLOAD = {"endpoint": ENDPOINT, "keys": {"p256dh": "p256dh-key", "auth": "auth-key"}}


@pytest.fixture(autouse=True)
def _no_vapid_keys() -> Iterator[None]:
    settings = get_settings()
    old = (settings.vapid_public_key, settings.vapid_private_key, settings.vapid_subject)
    settings.vapid_public_key = settings.vapid_private_key = settings.vapid_subject = ""
    yield
    settings.vapid_public_key, settings.vapid_private_key, settings.vapid_subject = old


@contextmanager
def vapid_keys(public: str = "pub-key", private: str = "priv-key") -> Iterator[None]:
    """Включить push на время теста: ключи живут в настройках, а не в БД."""
    settings = get_settings()
    old = (settings.vapid_public_key, settings.vapid_private_key, settings.vapid_subject)
    settings.vapid_public_key, settings.vapid_private_key = public, private
    settings.vapid_subject = "mailto:test@example.com"
    try:
        yield
    finally:
        settings.vapid_public_key, settings.vapid_private_key, settings.vapid_subject = old


def _subscriptions() -> list[PushSubscription]:
    session = _test_session_factory()
    try:
        return list(session.scalars(select(PushSubscription)).all())
    finally:
        session.close()


def test_config_reports_push_off_without_keys(client: TestClient) -> None:
    body = client.get("/api/push/config").json()
    assert body == {"enabled": False, "public_key": None}


def test_config_returns_public_key_when_configured(client: TestClient) -> None:
    with vapid_keys():
        assert client.get("/api/push/config").json() == {"enabled": True, "public_key": "pub-key"}


def test_subscribe_is_rejected_when_push_is_off(client: TestClient) -> None:
    # 503, а не 500: сервер настроен не полностью, и фронт покажет это словами
    assert client.post("/api/push/subscribe", json=PAYLOAD).status_code == 503
    assert _subscriptions() == []


def test_subscribe_stores_device_with_user_agent(client: TestClient) -> None:
    with vapid_keys():
        resp = client.post(
            "/api/push/subscribe", json=PAYLOAD, headers={"User-Agent": "Phone/1.0 (Android)"}
        )
    assert resp.status_code == 200, resp.text
    body = resp.json()
    assert body["user_agent"] == "Phone/1.0 (Android)"
    assert body["last_success_at"] is None

    (sub,) = _subscriptions()
    assert (sub.user_id, sub.endpoint, sub.p256dh, sub.auth) == (
        "1",
        ENDPOINT,
        "p256dh-key",
        "auth-key",
    )


def test_subscriptions_are_listed_and_removed(client: TestClient) -> None:
    with vapid_keys():
        sub_id = client.post("/api/push/subscribe", json=PAYLOAD).json()["id"]
        listed = client.get("/api/push/subscriptions").json()
        assert [item["id"] for item in listed] == [sub_id]
        assert client.delete(f"/api/push/subscriptions/{sub_id}").json() == {"ok": True}
    assert client.get("/api/push/subscriptions").json() == []


def test_unsubscribe_by_endpoint_is_idempotent(client: TestClient) -> None:
    with vapid_keys():
        client.post("/api/push/subscribe", json=PAYLOAD)
        assert client.request(
            "DELETE", "/api/push/subscribe", json={"endpoint": ENDPOINT}
        ).json() == {"ok": True}
        # повторная отписка — тоже успех: браузер мог потерять подписку сам
        assert client.request(
            "DELETE", "/api/push/subscribe", json={"endpoint": ENDPOINT}
        ).json() == {"ok": True}
    assert _subscriptions() == []


def test_same_endpoint_is_rebound_to_the_new_user(client: TestClient) -> None:
    """Один браузерный профиль, два аккаунта: подписка переезжает, а не двоится."""
    other = {"endpoint": ENDPOINT, "keys": {"p256dh": "чужой-ключ", "auth": "чужой-auth"}}
    with vapid_keys():
        client.post("/api/push/subscribe", json=PAYLOAD)
        with switch_user("other-user"):
            client.post("/api/push/subscribe", json=other)
            assert [s.user_id for s in _subscriptions()] == ["other-user"]
            assert client.get("/api/push/subscriptions").json() != []
        # прежний владелец устройство больше не видит и удалить по id не может
        assert client.get("/api/push/subscriptions").json() == []


def test_device_of_another_user_is_not_visible(client: TestClient) -> None:
    with vapid_keys():
        sub_id = client.post("/api/push/subscribe", json=PAYLOAD).json()["id"]
        with switch_user("other-user"):
            assert client.delete(f"/api/push/subscriptions/{sub_id}").status_code == 404
    assert len(_subscriptions()) == 1


@pytest.mark.parametrize(
    "schema",
    [
        {"endpoint": "short", "keys": {"p256dh": "a", "auth": "b"}},
        {"endpoint": ENDPOINT, "keys": {"p256dh": "", "auth": "b"}},
        {"endpoint": ENDPOINT},
    ],
)
def test_broken_payloads_are_rejected(client: TestClient, schema: dict[str, Any]) -> None:
    with vapid_keys():
        assert client.post("/api/push/subscribe", json=schema).status_code == 422


def test_requires_authentication(client: TestClient, monkeypatch: Any) -> None:
    """Устройства принадлежат пользователю: без сессии API их не показывает."""
    monkeypatch.delitem(app.dependency_overrides, require_user)
    assert client.get("/api/push/subscriptions").status_code == 401
