"""Тесты службы отправки (ТЗ 3.21).

Наружу не выходим: `webpush` подменён, а push-сервис отвечает так, как отвечает
настоящий — 201 на успех, 404/410 на отозванную подписку, 500 на сбой.
"""

from collections.abc import Iterator
from types import SimpleNamespace
from typing import Any

import pytest
from sqlalchemy import select

from app.config import get_settings
from app.models import AppSetting, PushSubscription, Task, User
from app.services import push as push_service
from app.services.push_texts import render, user_language
from tests.conftest import _test_session_factory  # type: ignore[attr-defined]

ENDPOINT = "https://push.example.com/sub/abc123"


@pytest.fixture
def db(monkeypatch: Any) -> Iterator[Any]:
    monkeypatch.setattr(push_service, "get_session_factory", lambda: _test_session_factory)
    session = _test_session_factory()
    try:
        yield session
    finally:
        session.close()


@pytest.fixture(autouse=True)
def vapid() -> Iterator[None]:
    settings = get_settings()
    old = (settings.vapid_public_key, settings.vapid_private_key, settings.vapid_subject)
    settings.vapid_public_key = "pub-key"
    settings.vapid_private_key = "priv-key"
    settings.vapid_subject = "mailto:test@example.com"
    try:
        yield
    finally:
        settings.vapid_public_key, settings.vapid_private_key, settings.vapid_subject = old


@pytest.fixture
def sent(monkeypatch: Any) -> list[dict[str, Any]]:
    """Записанные вызовы webpush: словарь подписки + параметры отправки."""
    calls: list[dict[str, Any]] = []

    def fake_webpush(info: Any, **kwargs: Any) -> Any:
        calls.append({"info": info, **kwargs})
        return SimpleNamespace(status_code=201)

    monkeypatch.setattr(push_service, "webpush", fake_webpush)
    return calls


def _fail_with(status: int) -> Any:
    def raiser(info: Any, **kwargs: Any) -> Any:
        from pywebpush import WebPushException

        raise WebPushException("сбой", response=SimpleNamespace(status_code=status, text=""))

    return raiser


def _device(
    db: Any, user_id: str = "1", endpoint: str = ENDPOINT, **extra: Any
) -> PushSubscription:
    sub = PushSubscription(
        user_id=user_id, endpoint=endpoint, p256dh="p256dh", auth="auth", **extra
    )
    db.add(sub)
    db.commit()
    return sub


def _payload() -> str:
    return push_service.build_payload(
        kind="task.review", title="Отчёт", body="Посмотрите, что сделано", url="/tasks/1", tag="t1"
    )


def test_push_is_off_without_keys(db: Any, sent: list[dict[str, Any]], monkeypatch: Any) -> None:
    settings = get_settings()
    settings.vapid_public_key = ""
    settings.vapid_private_key = ""
    _device(db)
    assert push_service.send_to_user("1", _payload()) == 0
    assert sent == []


def test_successful_delivery_marks_subscription(db: Any, sent: list[dict[str, Any]]) -> None:
    sub = _device(db)
    assert push_service.send_to_user("1", _payload()) == 1

    assert len(sent) == 1
    call = sent[0]
    assert call["info"]["endpoint"] == ENDPOINT
    assert call["info"]["keys"] == {"p256dh": "p256dh", "auth": "auth"}
    assert call["content_encoding"] == "aes128gcm"
    assert call["vapid_claims"]["sub"] == "mailto:test@example.com"

    db.refresh(sub)
    assert sub.last_success_at is not None
    assert sub.failure_count == 0


def test_gone_subscription_is_removed(db: Any, monkeypatch: Any) -> None:
    """404/410 — браузер отозвал подписку: держать её больше некому."""
    _device(db)
    monkeypatch.setattr(push_service, "webpush", _fail_with(410))
    assert push_service.send_to_user("1", _payload()) == 0
    assert db.scalars(select(PushSubscription)).all() == []


def test_transient_failure_keeps_subscription(db: Any, monkeypatch: Any) -> None:
    sub = _device(db)
    monkeypatch.setattr(push_service, "webpush", _fail_with(500))
    assert push_service.send_to_user("1", _payload()) == 0

    db.refresh(sub)
    assert sub.failure_count == 1
    assert db.scalars(select(PushSubscription)).all() != []


def test_subscription_dies_after_repeated_failures(db: Any, monkeypatch: Any) -> None:
    """Пять неудач подряд — устройство потеряно, шестого письма ему не нужно."""
    _device(db)
    monkeypatch.setattr(push_service, "webpush", _fail_with(500))
    for _ in range(push_service.MAX_FAILURES):
        push_service.send_to_user("1", _payload())
    assert db.scalars(select(PushSubscription)).all() == []


def test_resubscribe_resets_failure_counter(db: Any, monkeypatch: Any) -> None:
    """Вернувшееся устройство не должно умереть от старых ошибок."""
    sub = _device(db, failure_count=4)
    push_service.save_subscription(db, "1", ENDPOINT, "p256dh", "auth", "Phone/1.0")
    db.commit()
    assert sub.failure_count == 0


def test_delivery_goes_only_to_owner_devices(db: Any, sent: list[dict[str, Any]]) -> None:
    _device(db, user_id="1", endpoint=f"{ENDPOINT}-mine")
    _device(db, user_id="2", endpoint=f"{ENDPOINT}-foreign")
    assert push_service.send_to_user("1", _payload()) == 1
    assert sent[0]["info"]["endpoint"] == f"{ENDPOINT}-mine"


def test_payload_is_truncated_to_push_limit() -> None:
    """~4 КБ — потолок push-сервиса: тело усекаем, а не теряем уведомление."""
    import json

    long_body = "я" * 5000
    data = push_service.build_payload(
        kind="task.review", title="Отчёт", body=long_body, url="/tasks/1", tag="t1"
    )
    assert len(data.encode()) <= push_service.MAX_PAYLOAD
    decoded = json.loads(data)
    # заголовок и ссылка на месте — усекли только тело
    assert decoded["title"] == "Отчёт"
    assert decoded["url"] == "/tasks/1"
    assert decoded["body"].endswith("…")


# --- Тексты и язык ---


def test_language_chain_settings_then_sso_then_default(db: Any) -> None:
    db.add(User(id="u1", email="a@b.c", locale="en-US"))
    db.add(User(id="u2", email="a@b.c", locale="de"))
    db.commit()
    # настройка перебивает SSO; регион отбрасывается; неизвестный язык → ru
    db.add(AppSetting(user_id="u1", key="language", value="uk"))
    db.commit()
    assert user_language(db, "u1") == "uk"
    assert user_language(db, "u2") == "ru"
    assert user_language(db, "нет-такого") == "ru"


def test_render_fills_title_and_agent_name() -> None:
    title, body = render("task.claimed", "ru", title="Починить забор", name="Нави") or ("", "")
    assert title and "Починить забор" in body and "Нави" in body
    # неизвестный язык падает на ru, а неизвестный kind — не выдумывается
    assert render("task.claimed", "de", title="X", name="N") == render(
        "task.claimed", "ru", title="X", name="N"
    )
    assert render("task.unknown", "ru", title="X") is None


def test_render_clips_long_title() -> None:
    """Заголовок приходит от пользователя (до 500 символов), а уведомление — не место для него."""
    _, body = render("task.review", "ru", title="Заголовок " * 100) or ("", "")
    assert "…" in body
    assert len(body) <= 140


def test_notify_agent_review_sends_rendered_notification(
    db: Any, sent: list[dict[str, Any]]
) -> None:
    """Полный путь: задача в БД → текст на языке владельца → push на устройство."""
    import json

    db.add(User(id="1", email="a@b.c", locale="en"))
    db.add(Task(id=1, user_id="1", title="Финансовый отчёт"))
    db.commit()
    _device(db)

    push_service.notify_agent_review(1)
    assert len(sent) == 1
    data = json.loads(sent[0]["data"])
    assert data["kind"] == "task.review"
    assert data["url"] == "/tasks/1"
    assert "Финансовый отчёт" in data["body"]
    assert data["title"] == "Work awaits review"  # язык взят из SSO-локали


def test_notify_skips_unknown_task(db: Any, sent: list[dict[str, Any]]) -> None:
    _device(db)
    push_service.notify_agent_review(999)
    assert sent == []
