"""Unit-тесты consumer-PII scrubber для GlitchTip (#396) + Telegram bot-токен
redaction (#tgsupport review — CRITICAL: токен утекал в GlitchTip двумя
независимыми векторами, воспроизведёнными ревьюером на реальном событии
sentry_sdk: stack-frame locals (`include_local_variables=True`) и httpx-span
`data` (`HttpxIntegration`). Тесты ниже конструируют event-словари ровно в той
форме, в которой их реально отдаёт sentry_sdk (frames[].vars, spans[].data),
чтобы regression на этот конкретный CRITICAL не проскочил молча."""

import os

import pytest

os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")

from app.observability.sentry_scrub import (
    redact_telegram_bot_token,
    scrub_pii_event,
)


def test_redacts_pii_in_request_data() -> None:
    event = {
        "request": {
            "data": {
                "client_name": "Иван Иванов",
                "client_phone": "+79991234567",
                "client_email": "ivan@example.com",
                "address": "Екатеринбург, ул. Ленина 1",
            }
        }
    }
    out = scrub_pii_event(event, {})
    data = out["request"]["data"]
    assert data["client_name"] == "[REDACTED]"
    assert data["client_phone"] == "[REDACTED]"
    assert data["client_email"] == "[REDACTED]"
    # non-PII поле не трогаем
    assert data["address"] == "Екатеринбург, ул. Ленина 1"


def test_redacts_pii_in_extra() -> None:
    event = {
        "extra": {
            "phone": "+79990000000",
            "email": "x@y.ru",
            "name": "Пётр",
            "estimate_id": 42,
        }
    }
    out = scrub_pii_event(event, {})
    extra = out["extra"]
    assert extra["phone"] == "[REDACTED]"
    assert extra["email"] == "[REDACTED]"
    assert extra["name"] == "[REDACTED]"
    assert extra["estimate_id"] == 42


def test_redact_is_case_insensitive() -> None:
    event = {"extra": {"Client_Name": "Анна", "CLIENT_PHONE": "+7900"}}
    out = scrub_pii_event(event, {})
    assert out["extra"]["Client_Name"] == "[REDACTED]"
    assert out["extra"]["CLIENT_PHONE"] == "[REDACTED]"


def test_redacts_nested_pii_in_contexts() -> None:
    event = {"contexts": {"trace": {"op": "http"}, "consumer": {"client_email": "z@z.ru"}}}
    out = scrub_pii_event(event, {})
    assert out["contexts"]["consumer"]["client_email"] == "[REDACTED]"
    # вложенный non-PII контекст не трогаем
    assert out["contexts"]["trace"]["op"] == "http"


def test_leaves_non_pii_untouched() -> None:
    event = {
        "request": {"data": {"region": "66", "area_sqm": 50}},
        "extra": {"job": "geocode"},
        "level": "error",
    }
    out = scrub_pii_event(event, {})
    assert out["request"]["data"] == {"region": "66", "area_sqm": 50}
    assert out["extra"] == {"job": "geocode"}
    assert out["level"] == "error"


def test_handles_missing_sections() -> None:
    out = scrub_pii_event({}, {})
    assert out == {}


def test_handles_none_and_non_dict_sections() -> None:
    event = {"request": None, "extra": None, "contexts": "not-a-dict"}
    # не должно бросать исключений
    out = scrub_pii_event(event, {})
    assert out is event


def test_returns_event_not_none() -> None:
    """before_send должен вернуть event (не None) — иначе SDK дропнет отчёт."""
    event = {"request": {"data": {"client_name": "X"}}}
    out = scrub_pii_event(event, {})
    assert out is not None
    assert out is event


# ── Telegram bot-token redaction (#tgsupport review, CRITICAL) ──────────────

_LEAKED_TOKEN_URL = "https://api.telegram.org/bot8663867262:AAExampleSecretPartAbCdEf123/getMe"


def test_redacts_token_in_stack_frame_locals() -> None:
    """Вектор #1 (ревьюер): `include_local_variables=True` кладёт locals
    `TelegramClient._request` (`self`, `url`) в traceback frame `vars`."""
    event = {
        "exception": {
            "values": [
                {
                    "type": "NetworkError",
                    "stacktrace": {
                        "frames": [
                            {
                                "function": "_request",
                                "vars": {
                                    "url": _LEAKED_TOKEN_URL,
                                    "self": {"_base": _LEAKED_TOKEN_URL.rsplit("/", 1)[0]},
                                    "method": "getMe",
                                },
                            }
                        ]
                    },
                }
            ]
        }
    }
    out = redact_telegram_bot_token(event, {})
    assert out is not None
    frame_vars = out["exception"]["values"][0]["stacktrace"]["frames"][0]["vars"]
    assert "8663867262:AAExampleSecretPartAbCdEf123" not in frame_vars["url"]
    assert "8663867262:AAExampleSecretPartAbCdEf123" not in frame_vars["self"]["_base"]
    assert frame_vars["url"] == "https://api.telegram.org/bot[REDACTED]/getMe"
    # Не PII/секрет — остаётся как есть.
    assert frame_vars["method"] == "getMe"


def test_redacts_token_in_httpx_span_data() -> None:
    """Вектор #2 (ревьюер): `HttpxIntegration` кладёт полный request URL в span
    `data` независимо от traceback — `traces_sample_rate=0.0` спасает сейчас, но
    редактор — belt-and-suspenders на случай если трейсинг когда-нибудь включат."""
    event = {
        "spans": [
            {
                "op": "http.client",
                "description": "POST " + _LEAKED_TOKEN_URL,
                "data": {"url": _LEAKED_TOKEN_URL, "http.method": "POST"},
            }
        ]
    }
    out = redact_telegram_bot_token(event, {})
    assert out is not None
    span = out["spans"][0]
    assert "8663867262:AAExampleSecretPartAbCdEf123" not in span["data"]["url"]
    assert "8663867262:AAExampleSecretPartAbCdEf123" not in span["description"]
    assert span["data"]["http.method"] == "POST"


def test_redacts_token_in_breadcrumb_message() -> None:
    """httpx-логгер (INFO, до нашего getLogger('httpx').setLevel(WARNING) в
    tgbot_main) может всплыть breadcrumb'ом с полным URL через LoggingIntegration."""
    event = {
        "breadcrumbs": {
            "values": [
                {
                    "category": "httpx",
                    "message": f'HTTP Request: POST {_LEAKED_TOKEN_URL} "HTTP/1.1 401"',
                }
            ]
        }
    }
    out = redact_telegram_bot_token(event, {})
    assert out is not None
    msg = out["breadcrumbs"]["values"][0]["message"]
    assert "8663867262:AAExampleSecretPartAbCdEf123" not in msg
    assert "/bot[REDACTED]/getMe" in msg


def test_redact_token_leaves_unrelated_urls_and_strings_untouched() -> None:
    event = {"extra": {"public_url": "https://gendsgn.ru/trade-in", "region": "66"}}
    out = redact_telegram_bot_token(event, {})
    assert out is not None
    assert out["extra"]["public_url"] == "https://gendsgn.ru/trade-in"
    assert out["extra"]["region"] == "66"


def test_redact_token_handles_non_dict_event() -> None:
    assert redact_telegram_bot_token(None, {}) is None  # type: ignore[arg-type]


# Голая форма `<id>:<secret>` без префикса `/bot` — так токен выглядит в локали
# `token` конструктора TelegramClient. Штатно в event не попадает
# (include_local_variables=False в tgbot_main), но редактор обязан покрывать и
# этот случай: иначе рубеж против утечки токена всего один — флаг SDK.
_LEAKED_TOKEN_BARE = "8663867262:AAFakeSecretPartForTestsOnly1234567"


def test_redacts_bare_token_without_bot_prefix() -> None:
    event = {
        "logentry": {"message": f"init failed for {_LEAKED_TOKEN_BARE}"},
        "exception": {
            "values": [{"stacktrace": {"frames": [{"vars": {"token": _LEAKED_TOKEN_BARE}}]}}]
        },
    }
    out = redact_telegram_bot_token(event, {})
    assert out is not None
    assert "AAFakeSecretPartForTestsOnly1234567" not in repr(out)
    frame = out["exception"]["values"][0]["stacktrace"]["frames"][0]
    assert frame["vars"]["token"] == "[REDACTED]"


@pytest.mark.parametrize(
    "benign",
    [
        "chat_id:12345",
        "ratio 3:4",
        "2026-07-16T10:00:00",
        "postgresql://user:pass@postgres:5432/tradein",
    ],
)
def test_bare_token_redaction_leaves_benign_colon_strings_untouched(benign: str) -> None:
    """Голая регулярка не должна бить по любым `x:y` — иначе диагностика ослепнет."""
    out = redact_telegram_bot_token({"logentry": {"message": benign}}, {})
    assert out is not None
    assert out["logentry"]["message"] == benign


def test_composed_before_send_scrubs_pii_and_token_together() -> None:
    """Композиция, реально используемая в `app.tgbot_main._before_send`: PII-scrub
    (ключ-based) И token-redaction (regex full-text) применяются оба, не заменяя
    друг друга — разные классы секретов, разные механизмы обнаружения."""
    event = {
        "request": {"data": {"client_phone": "+79991234567"}},
        "exception": {
            "values": [
                {
                    "stacktrace": {
                        "frames": [{"vars": {"url": _LEAKED_TOKEN_URL}}],
                    }
                }
            ]
        },
    }

    def composed_before_send(evt, hint):
        scrubbed = scrub_pii_event(evt, hint)
        if scrubbed is None:
            return None
        return redact_telegram_bot_token(scrubbed, hint)

    out = composed_before_send(event, {})
    assert out is not None
    assert out["request"]["data"]["client_phone"] == "[REDACTED]"
    frame_url = out["exception"]["values"][0]["stacktrace"]["frames"][0]["vars"]["url"]
    assert "8663867262:AAExampleSecretPartAbCdEf123" not in frame_url
