"""Webhook от gnexus-auth (ТЗ 3.16): подпись, события профиля, отзыв MCP-токенов."""

import hashlib
import hmac
import json
import time
from typing import Any

from sqlalchemy import select

from app.models import McpToken, User
from tests.conftest import _test_session_factory

WEBHOOK_SECRET = "whsec-test"


def _signed_headers(
    body: str, event_type: str = "webhook.test", secret: str = WEBHOOK_SECRET
) -> dict[str, str]:
    ts = str(int(time.time()))
    sig = hmac.new(secret.encode(), f"{ts}.{body}".encode(), hashlib.sha256).hexdigest()
    return {
        "X-GNexus-Event-Id": "evt-1",
        "X-GNexus-Event-Type": event_type,
        "X-GNexus-Event-Timestamp": ts,
        "X-GNexus-Signature": f"t={ts},v1={sig}",
    }


def _enable_webhook(monkeypatch: Any) -> None:
    """GAUTH_WEBHOOK_SECRET читается через get_settings в момент запроса."""
    import app.auth.webhooks as wh

    class _Settings:
        gauth_webhook_secret = WEBHOOK_SECRET

    monkeypatch.setattr(wh, "get_settings", lambda: _Settings())


def _post(client: Any, payload: dict, event_type: str, secret: str = WEBHOOK_SECRET) -> Any:
    body = json.dumps(payload)
    return client.post(
        "/auth/webhook",
        content=body,
        headers={"Content-Type": "application/json", **_signed_headers(body, event_type, secret)},
    )


def test_webhook_disabled_without_secret(client: Any, monkeypatch: Any) -> None:
    # секрет пуст явно: в dev/проде Settings читает .env, где секрет задан
    import app.auth.webhooks as wh

    class _Settings:
        gauth_webhook_secret = ""

    monkeypatch.setattr(wh, "get_settings", lambda: _Settings())
    resp = _post(client, {"type": "webhook.test"}, "webhook.test")
    assert resp.status_code == 503


def test_webhook_test_event_accepted(client: Any, monkeypatch: Any) -> None:
    _enable_webhook(monkeypatch)
    resp = _post(client, {"type": "webhook.test", "data": {}}, "webhook.test")
    assert resp.status_code == 200
    assert resp.json() == {"ok": True}


def test_webhook_invalid_signature_rejected(client: Any, monkeypatch: Any) -> None:
    _enable_webhook(monkeypatch)
    body = json.dumps({"type": "webhook.test"})
    resp = client.post(
        "/auth/webhook",
        content=body,
        headers={
            "Content-Type": "application/json",
            **_signed_headers(body, "webhook.test", "wrong-secret"),
        },
    )
    assert resp.status_code == 400


def test_webhook_garbage_payload_rejected(client: Any, monkeypatch: Any) -> None:
    _enable_webhook(monkeypatch)
    body = "not json"
    resp = client.post(
        "/auth/webhook",
        content=body,
        headers={"Content-Type": "application/json", **_signed_headers(body)},
    )
    assert resp.status_code == 400


def test_user_blocked_revokes_mcp_tokens(client: Any, monkeypatch: Any) -> None:
    _enable_webhook(monkeypatch)
    session = _test_session_factory()
    try:
        session.add(McpToken(user_id="u1", token_hash="a" * 64, label="x"))
        session.add(McpToken(user_id="u2", token_hash="b" * 64, label="y"))
        session.commit()

        resp = _post(
            client,
            {"type": "user.blocked", "data": {"user": {"id": "u1"}}},
            "user.blocked",
        )
        assert resp.status_code == 200

        remaining = session.scalars(select(McpToken).where(McpToken.user_id == "u1")).all()
        assert remaining == []
        others = session.scalars(select(McpToken).where(McpToken.user_id == "u2")).all()
        assert len(others) == 1
    finally:
        session.close()


def test_user_deleted_revokes_mcp_tokens(client: Any, monkeypatch: Any) -> None:
    _enable_webhook(monkeypatch)
    session = _test_session_factory()
    try:
        session.add(McpToken(user_id="u1", token_hash="a" * 64, label="x"))
        session.commit()

        # user_id в target (после удаления персональные данные не передаются)
        resp = _post(client, {"type": "user.deleted", "target": {"user_id": "u1"}}, "user.deleted")
        assert resp.status_code == 200
        assert session.scalars(select(McpToken).where(McpToken.user_id == "u1")).all() == []
    finally:
        session.close()


def test_email_changed_updates_user(client: Any, monkeypatch: Any) -> None:
    _enable_webhook(monkeypatch)
    session = _test_session_factory()
    try:
        session.add(User(id="u1", email="old@example.com", avatar_url=None, locale="ru"))
        session.commit()

        resp = _post(
            client,
            {
                "type": "user.email_changed",
                "data": {"user": {"id": "u1"}, "new_email": "new@example.com"},
            },
            "user.email_changed",
        )
        assert resp.status_code == 200
        session.expire_all()
        assert session.get(User, "u1").email == "new@example.com"
    finally:
        session.close()


def test_profile_updated_syncs_locale_and_avatar(client: Any, monkeypatch: Any) -> None:
    _enable_webhook(monkeypatch)
    session = _test_session_factory()
    try:
        session.add(User(id="u1", email="u@example.com", avatar_url=None, locale="ru"))
        session.commit()

        resp = _post(
            client,
            {
                "type": "user.profile_updated",
                "data": {
                    "user": {"id": "u1"},
                    "profile": {"locale": "uk", "avatar_url": "https://sso/avatar.jpg"},
                },
            },
            "user.profile_updated",
        )
        assert resp.status_code == 200
        session.expire_all()
        user = session.get(User, "u1")
        assert user.locale == "uk"
        assert user.avatar_url == "https://sso/avatar.jpg"
    finally:
        session.close()


def test_unknown_event_type_is_acked(client: Any, monkeypatch: Any) -> None:
    """Незнакомый тип SSO подтверждаем 200 — без retry-шторма с его стороны."""
    _enable_webhook(monkeypatch)
    resp = _post(
        client,
        {"type": "group.user_added", "data": {"user": {"id": "u1"}}},
        "group.user_added",
    )
    assert resp.status_code == 200
