"""§19 redaction tests (#960) — самый важный слой безопасности.

Покрываем: каждый тип RU PII, hard-block is_confidential, allowlist-поведение
(SafePayload как единственный вход), и шов RU-hosted провайдера (is_external=False).
"""

from __future__ import annotations

import pytest

from app.services.llm.redaction import (
    RedactionRefusedError,
    SafePayload,
    scrub_safe_payload,
    scrub_text,
)

# ── Per-PII-type scrub ────────────────────────────────────────────────────────


def test_scrub_phone_ru() -> None:
    out = scrub_text("Звоните +7 (912) 345-67-89 срочно")
    assert "[REDACTED:phone]" in out
    assert "912" not in out


def test_scrub_phone_8_prefix() -> None:
    out = scrub_text("тел 8 912 345 67 89")
    assert "[REDACTED:phone]" in out
    assert "345" not in out


def test_scrub_email() -> None:
    out = scrub_text("пишите ivan.petrov@example.ru пожалуйста")
    assert "[REDACTED:email]" in out
    assert "example.ru" not in out


def test_scrub_snils() -> None:
    out = scrub_text("СНИЛС 112-233-445 95 в деле")
    assert "[REDACTED:snils]" in out
    assert "112-233-445" not in out


def test_scrub_inn_12() -> None:
    out = scrub_text("ИНН физлица 500100732259 проверен")
    assert "[REDACTED:inn]" in out
    assert "500100732259" not in out


def test_scrub_inn_10() -> None:
    out = scrub_text("ИНН юрлица 7707083893 в ЕГРЮЛ")
    assert "[REDACTED:inn]" in out
    assert "7707083893" not in out


def test_scrub_passport() -> None:
    out = scrub_text("паспорт 6512 123456 выдан")
    assert "[REDACTED:passport]" in out
    assert "123456" not in out


def test_scrub_fullname() -> None:
    out = scrub_text("ответственный Иванов Иван Иванович подписал")
    assert "[REDACTED:name]" in out
    assert "Иванович" not in out


# ── #1207: regex-дыры в первичном scrub'е (P2-security) ─────────────────────────


def test_scrub_phone_bare_11_digits_starting_with_7() -> None:
    """Copy-paste форма «79221234567» (11 цифр, без + и без 8) — самая частая дыра.

    _PHONE_RE требует «+7»/«\\b8» + разделитель → этот вариант пропускался и уходил
    в OpenAI как есть. _PHONE_BARE_RE закрывает дыру.
    """
    out = scrub_text("позвоните 79221234567 договоримся")
    assert "[REDACTED:phone]" in out
    assert "79221234567" not in out


def test_scrub_phone_bare_11_digits_starting_with_8() -> None:
    """Аналогично — «89221234567» без разделителей."""
    out = scrub_text("моб 89221234567 в любое время")
    assert "[REDACTED:phone]" in out
    assert "89221234567" not in out


def test_scrub_fullname_caps_format() -> None:
    """Стандарт ЕГРН/паспортных выписок «ИВАНОВ ИВАН ИВАНОВИЧ» — старый regex требовал
    строчные после первой буквы → каполная форма уходила как есть."""
    out = scrub_text("собственник ИВАНОВ ИВАН ИВАНОВИЧ зарегистрирован")
    assert "[REDACTED:name]" in out
    assert "ИВАНОВИЧ" not in out


def test_scrub_fullname_mixed_caps_and_titlecase() -> None:
    """Смешанная форма «ИВАНОВ Иван Иванович» — тоже должна срабатывать."""
    out = scrub_text("по доверенности ИВАНОВ Иван Иванович подписал")
    assert "[REDACTED:name]" in out
    assert "ИВАНОВ" not in out


def test_scrub_snils_bare_11_digits() -> None:
    """СНИЛС без разделителей «12345678901» — не покрывался ни phone (нет 7/8 префикса),
    ни ИНН (длина не 10/12)."""
    out = scrub_text("СНИЛС 12345678901 в личном деле")
    assert "[REDACTED:snils]" in out
    assert "12345678901" not in out


def test_phone_bare_wins_over_snils_bare_for_7_prefix() -> None:
    """Конфликт _PHONE_BARE_RE vs _SNILS_BARE_RE: 11 цифр, начинаются с 7 → phone."""
    out = scrub_text("79221234567")
    assert "[REDACTED:phone]" in out
    assert "[REDACTED:snils]" not in out


def test_inn_10_digits_not_caught_as_snils_bare() -> None:
    """10-значный ИНН не должен попадать в _SNILS_BARE_RE (тот требует ровно 11)."""
    out = scrub_text("ИНН 7707083893 действующий")
    assert "[REDACTED:inn]" in out
    assert "[REDACTED:snils]" not in out


def test_scrub_multiple_pii_in_one_text() -> None:
    out = scrub_text("Иванов Иван Иванович, +7 912 345 67 89, mail@x.ru")
    assert "[REDACTED:name]" in out
    assert "[REDACTED:phone]" in out
    assert "[REDACTED:email]" in out


def test_scrub_preserves_safe_text() -> None:
    """Безопасный текст (без PII) не трогается."""
    safe = "Зона Ц-1, максимальная этажность 16, процент застройки 60."
    assert scrub_text(safe) == safe


def test_scrub_empty_string_noop() -> None:
    assert scrub_text("") == ""


# ── Hard-block: is_confidential ───────────────────────────────────────────────


def test_confidential_payload_refused_for_external() -> None:
    """is_confidential=True + external провайдер → RedactionRefusedError (НЕ отправляем)."""
    payload = SafePayload(text="секретная аналитика", is_confidential=True)
    with pytest.raises(RedactionRefusedError):
        scrub_safe_payload(payload, is_external=True)


def test_non_confidential_payload_passes_external() -> None:
    """is_confidential=False → проходит (и скрабится)."""
    payload = SafePayload(text="публичный текст про зону Ц-1", is_confidential=False)
    out = scrub_safe_payload(payload, is_external=True)
    assert out.text == "публичный текст про зону Ц-1"
    assert out.is_confidential is False


# ── Scrub применяется к text И fields на external-пути ─────────────────────────


def test_scrub_safe_payload_scrubs_text_and_fields() -> None:
    payload = SafePayload(
        text="контакт +7 912 345 67 89",
        fields={"note": "почта a@b.ru", "floors": 16, "code": "Ц-1"},
    )
    out = scrub_safe_payload(payload, is_external=True)
    assert "[REDACTED:phone]" in out.text
    assert "[REDACTED:email]" in out.fields["note"]
    # Не-строковые и безопасные значения сохранены.
    assert out.fields["floors"] == 16
    assert out.fields["code"] == "Ц-1"


def test_scrub_nested_fields() -> None:
    """Рекурсивный scrub вложенных list/dict в fields."""
    payload = SafePayload(fields={"items": [{"phone": "+7 912 345 67 89"}, "a@b.ru"]})
    out = scrub_safe_payload(payload, is_external=True)
    assert "[REDACTED:phone]" in out.fields["items"][0]["phone"]
    assert "[REDACTED:email]" in out.fields["items"][1]


# ── RU-hosted seam: is_external=False ─────────────────────────────────────────


def test_ru_hosted_provider_skips_scrub_and_allows_confidential() -> None:
    """is_external=False (будущий RU-hosted): scrub не обязателен, confidential проходит.

    Данные не покидают РФ → §19 не нарушается. Это задокументированный шов.
    """
    payload = SafePayload(
        text="контакт +7 912 345 67 89",
        is_confidential=True,
    )
    out = scrub_safe_payload(payload, is_external=False)
    # Не бросает, текст НЕ скраблен (RU-провайдер может получить как есть).
    assert out is payload
    assert "+7 912 345 67 89" in out.text


# ── Allowlist contract: SafePayload — единственный вход ────────────────────────


def test_safe_payload_defaults_are_empty_and_non_confidential() -> None:
    """По умолчанию SafePayload пуст и не-конфиденциален — безопасный нейтральный старт."""
    p = SafePayload()
    assert p.text == ""
    assert p.fields == {}
    assert p.is_confidential is False


# ── #1640: INN context-guard — large bare numbers must NOT be redacted ────────


def test_large_sum_10_digits_not_redacted_without_inn_keyword() -> None:
    """Слитная 10-значная сумма (1 200 000 000 руб) без якоря «ИНН» — НЕ редактируется.

    Регрессия #1640: старый _INN_RE ловил любые 10/12 цифр → «1200000000» уходило в
    [REDACTED:inn]. Новый паттерн требует маркер ИНН/inn перед числом.
    """
    text = "стоимость участка составила 1200000000 рублей по договору"
    out = scrub_text(text)
    assert out == text, f"bare large sum was incorrectly redacted: {out!r}"


def test_large_sum_12_digits_not_redacted_without_inn_keyword() -> None:
    """12-значная слитная сумма без якоря — НЕ редактируется."""
    text = "капитализация компании 123456789012 рублей"
    out = scrub_text(text)
    assert out == text


def test_round_billion_not_redacted() -> None:
    """Круглые суммы без контекста — НЕ редактируются."""
    for amount in ("5000000000", "1000000000", "9876543210"):
        out = scrub_text(f"сумма {amount} руб")
        assert "[REDACTED:inn]" not in out, f"amount {amount!r} was falsely redacted"


def test_inn_with_keyword_is_redacted() -> None:
    """Валидный ИНН с явным маркером «ИНН» — редактируется."""
    out = scrub_text("ИНН 7707083893 зарегистрирован в ЕГРЮЛ")
    assert "[REDACTED:inn]" in out
    assert "7707083893" not in out


def test_inn_12_with_keyword_is_redacted() -> None:
    """Валидный 12-значный ИНН физлица с маркером — редактируется."""
    out = scrub_text("ИНН физлица 500100732259 подтверждён")
    assert "[REDACTED:inn]" in out
    assert "500100732259" not in out


def test_inn_keyword_latin_case_insensitive() -> None:
    """Маркер 'inn' (латиница, любой регистр) тоже активирует редакцию."""
    out = scrub_text("inn: 7707083893")
    assert "[REDACTED:inn]" in out


def test_inn_bad_checksum_with_keyword_not_redacted() -> None:
    """Число с маркером «ИНН», но не прошедшее checksum, НЕ редактируется.

    Предотвращает ложные срабатывания на «ИНН 1234567890» (неверная контрольная цифра).
    """
    out = scrub_text("ИНН 1234567890 (некорректный)")
    assert "[REDACTED:inn]" not in out
    assert "1234567890" in out


# ── #1640 follow-up: word-boundary guards ((?<!\w)/(?!\w)) ───────────────────


def test_inn_digits_alpha_prefix_not_redacted() -> None:
    """Цифры ИНН как суффикс буквенно-цифрового токена НЕ редактируются.

    Регрессия: (?<!\\d) не защищал от «ИНН ref7707083893» — 'f' не цифра, старый
    lookaround пропускал. Новый (?<!\\w) блокирует любой word-char перед цифровым блоком.
    """
    out = scrub_text("ИНН ref7707083893")
    assert "[REDACTED:inn]" not in out
    assert "7707083893" in out


def test_inn_digits_alpha_suffix_not_redacted() -> None:
    """Цифры ИНН как префикс буквенно-цифрового токена НЕ редактируются.

    Регрессия: старый (?!\\d) не блокировал буквенный суффикс — «7707083893more»
    совпадало, т.к. 'm' не цифра. Новый (?!\\w) закрывает alpha-суффикс.
    """
    out = scrub_text("ИНН 7707083893more")
    assert "[REDACTED:inn]" not in out
    assert "7707083893" in out


def test_inn_digits_embedded_in_alphanumeric_token_not_redacted() -> None:
    """ИНН-подобный блок цифр внутри алфавитно-цифрового токена НЕ редактируется.

    «ИНН ref7707083893xyz» — цифры с обеих сторон окружены word-char'ами.
    """
    out = scrub_text("ИНН ref7707083893xyz")
    assert "[REDACTED:inn]" not in out


def test_inn_alpha_prefix_on_12digit_block_not_redacted() -> None:
    """12-значный валидный ИНН с буквенным префиксом (без пробела) НЕ редактируется.

    «ИНН ABC500100732259» — 'C' является word-char, (?<!\\w) блокирует совпадение.
    """
    out = scrub_text("ИНН ABC500100732259")
    assert "[REDACTED:inn]" not in out
    assert "500100732259" in out


def test_inn_standalone_with_punctuation_is_redacted() -> None:
    """Валидный ИНН, отделённый знаком препинания (не word-char), редактируется.

    Двоеточие, запятая, скобка — не \\w → (?<!\\w) пропускает. Это корректное поведение.
    """
    out = scrub_text("ИНН:7707083893 в реестре")
    assert "[REDACTED:inn]" in out
    assert "7707083893" not in out


def test_inn_standalone_at_end_of_string_is_redacted() -> None:
    """Валидный ИНН в конце строки без символов после редактируется.

    Конец строки — не \\w → (?!\\w) пропускает. Базовый smoke.
    """
    out = scrub_text("ИНН 7707083893")
    assert "[REDACTED:inn]" in out
    assert "7707083893" not in out
