# -*- coding: utf-8 -*-
"""
Тесты, нацеленные на РЕАЛЬНЫЕ БАГИ и непокрытые случаи.
Каждый тест сначала показывает проблему, потом (после фикса) должен проходить.
"""
import asyncio

import pytest
from pydantic import BaseModel

from request_pydantic_decorator import (
    validate_response,
    ResponseValidator,
    Exceptions,
    StatusCodeError,
    HeaderError,
    header_equals,
    header_contains,
)
from conftest import FakeResponse, make_response


# ============================================================================
# БАГ 1: header_equals / header_contains — оператор `or` съедает пустую строку
#
# Код: v = headers.get(header_name) or headers.get(key)
# Если значение заголовка пустая строка "", то `"" or ...` → переходит ко
# второму варианту, который возвращает None → условие считает заголовок
# отсутствующим. Это НЕПРАВИЛЬНО: заголовок есть, просто значение пустое.
# ============================================================================


class TestBugHeaderEmptyValue:
    def test_header_equals_empty_value_should_match(self):
        """header_equals('X-Empty', '') должен вернуть True, если заголовок есть и равен ''."""
        cond = header_equals("X-Empty", "")
        r = make_response(headers={"X-Empty": ""})
        # BUG: до фикса возвращает False, потому что `"" or ...` даёт None
        assert cond(r) is True

    def test_header_contains_empty_substring_should_match(self):
        """header_contains('X-Val', '') — пустая подстрока всегда содержится."""
        cond = header_contains("X-Val", "")
        r = make_response(headers={"X-Val": "something"})
        assert cond(r) is True

    def test_header_equals_falsy_value_zero(self):
        """Заголовок со значением '0' — тоже falsy при `or`."""
        cond = header_equals("X-Count", "0")
        r = make_response(headers={"X-Count": "0"})
        # BUG: "0" is falsy? No, "0" is truthy in Python, so this is actually OK
        # But let's test to be sure
        assert cond(r) is True


# ============================================================================
# БАГ 2: HeadersValueValidator.validate() — .lower() на не-строке
#
# Если dict заголовков содержит не-строковое значение (int, bytes, None),
# строка `value = headers[actual_key].lower()` упадёт с AttributeError.
# Реальные HTTP-библиотеки всегда возвращают строки, но dict(response.headers)
# может содержать не-строки при мокинге или нестандартных объектах.
# ============================================================================


class TestBugNonStringHeaderValues:
    def test_headers_value_int_value_should_not_crash(self):
        """Валидатор не должен падать с AttributeError на нестроковом значении."""
        v = (
            ResponseValidator()
            .headers_value()
            .for_header("X-Num")
            .whitelist(["42"])
        )
        r = make_response(headers={"X-Num": 42})
        # BUG: до фикса — AttributeError: 'int' object has no attribute 'lower'
        # После фикса: значение приводится к str, "42" в whitelist → проходит
        v._validate_response(r)

    def test_headers_key_validator_with_non_string_keys(self):
        """Маловероятно, но dict может содержать нестроковые ключи."""
        v = ResponseValidator().headers_key().blacklist(["bad"])
        # Заголовок-число — lower() упадёт
        r = make_response(headers={"Content-Type": "text/html"})
        v._validate_response(r)  # Должен пройти без ошибки


# ============================================================================
# БАГ 3: e.response = response на исключениях с __slots__
#
# Если исключение использует __slots__ без 'response', присвоение
# `e.response = response` упадёт с AttributeError.
# ============================================================================


class TestBugExceptionWithSlots:
    def test_exception_with_slots_does_not_crash_decorator(self):
        """Если raise_as бросает исключение со __slots__, декоратор не должен падать."""

        class SlottedException(Exception):
            __slots__ = ("code",)
            def __init__(self, msg="fail"):
                super().__init__(msg)
                self.code = 500

        validator = (
            ResponseValidator()
            .status_code()
            .whitelist([200], raise_as=SlottedException)
        )

        @validate_response(validator=validator)
        def get():
            return make_response(status_code=500)

        # BUG: до фикса — AttributeError при попытке e.response = response
        # потому что SlottedException использует __slots__ без 'response'
        with pytest.raises(SlottedException):
            get()

    async def test_async_exception_with_slots(self):
        """То же для async."""

        class SlottedException(Exception):
            __slots__ = ()

        validator = (
            ResponseValidator()
            .status_code()
            .whitelist([200], raise_as=SlottedException)
        )

        @validate_response(validator=validator)
        async def get():
            return make_response(status_code=403)

        with pytest.raises(SlottedException):
            await get()


# ============================================================================
# БАГ 4: raise_as с исключением, которое НЕ принимает строковый аргумент
#
# Код: raise self._raise_as(self._raise_message)
# Если raise_as — класс с __init__(self, code: int), а raise_message — строка,
# то TypeError при создании экземпляра.
# ============================================================================


class TestBugRaiseAsIncompatibleInit:
    def test_raise_as_with_custom_init_no_message(self):
        """raise_as() без raise_message: вызовет raise_as() — если __init__ требует аргумент, упадёт."""

        class CustomError(Exception):
            def __init__(self, code: int = 0):
                super().__init__(f"Error code: {code}")
                self.code = code

        validator = (
            ResponseValidator()
            .status_code()
            .whitelist([200], raise_as=CustomError)
        )
        r = make_response(status_code=500)
        # Должен вызвать CustomError() с дефолтным code=0
        with pytest.raises(CustomError):
            validator._validate_response(r)

    def test_raise_as_with_message_incompatible_init(self):
        """raise_as с raise_message — вызовет raise_as(raise_message).
        Python не проверяет аннотации типов в рантайме, поэтому строка
        просто попадёт в code: int. Реальная проблема возникнет только
        если __init__ действительно требует другой тип (например, **kwargs)."""

        class StrictError(Exception):
            def __init__(self, *, code: int):
                """Принимает ТОЛЬКО keyword-аргумент — позиционный вызов упадёт."""
                super().__init__(f"code={code}")
                self.code = code

        validator = (
            ResponseValidator()
            .status_code()
            .whitelist([200], raise_as=StrictError, raise_message="текст ошибки")
        )
        r = make_response(status_code=500)
        # raise StrictError("текст ошибки") → TypeError: keyword-only argument
        with pytest.raises(TypeError):
            validator._validate_response(r)


# ============================================================================
# БАГ 5: Второй вызов status_code() перезаписывает первый
#
# ResponseValidator хранит один _status_code_validator. Если пользователь
# цепочкой добавит и whitelist, и blacklist — второй перезапишет первый.
# ============================================================================


class TestValidatorOverwrite:
    def test_second_status_code_call_overwrites_first(self):
        """Второй вызов .status_code().blacklist() перезаписывает .status_code().whitelist()."""
        v = (
            ResponseValidator()
            .status_code().whitelist([200, 201])  # этот должен работать
            .status_code().blacklist([500])        # этот ПЕРЕЗАПИСЫВАЕТ whitelist
        )
        r = make_response(status_code=404)
        # whitelist [200, 201] потерян! 404 пройдёт, потому что он не в blacklist [500]
        v._validate_response(r)  # НЕ бросит, хотя пользователь мог ожидать ошибку


# ============================================================================
# БАГ 6: validate_response — validator проходит, но model.json() падает.
# При raise_exceptions=False ошибка от json() ловится. Но response в
# ValidateResponse должен быть всё равно.
# ============================================================================


class TestModelFailAfterValidatorPass:
    def test_validator_pass_then_model_fail_raise_exceptions_false(self):
        """Валидатор прошёл (200), но Pydantic упал — error должен быть, response тоже."""

        class Strict(BaseModel):
            id: int
            name: str

        validator = ResponseValidator().status_code().whitelist([200])

        @validate_response(model=Strict, body_type="json", validator=validator, raise_exceptions=False)
        def get():
            return make_response(status_code=200, json_body={"id": "not_int"})

        result = get()
        assert result.response is not None
        assert result.response.status_code == 200
        assert result.error is not None
        assert result.validate is None


# ============================================================================
# БАГ 7: Concurrent async с raise_exceptions=True — исключения не смешиваются
# ============================================================================


class TestConcurrentExceptionIsolation:
    async def test_concurrent_raises_isolated(self):
        """Каждая задача получает своё исключение с правильным response."""
        validator = ResponseValidator().status_code().whitelist([200])

        @validate_response(validator=validator)
        async def get(code: int):
            return make_response(status_code=code, url=f"https://api.test/{code}")

        async def catch(code: int):
            try:
                await get(code)
                return None
            except Exception as e:
                return e

        results = await asyncio.gather(*[catch(c) for c in [500, 502, 503, 504, 400]])
        for i, (code, exc) in enumerate(zip([500, 502, 503, 504, 400], results)):
            assert exc is not None
            assert isinstance(exc, StatusCodeError)
            resp = getattr(exc, "response", None)
            assert resp is not None, f"Task {i}: response is None"
            assert resp.status_code == code, f"Task {i}: expected {code}, got {resp.status_code}"


# ============================================================================
# БАГ 8: redirect whitelist с dict-паттерном (query params)
# ============================================================================


class TestRedirectDictPattern:
    def test_redirect_whitelist_with_query_params_dict(self):
        """Проверяем, что dict-паттерн ищет query-параметры в URL."""
        v = ResponseValidator().redirect().whitelist([{"token": "abc"}])
        r = FakeResponse(
            url="https://example.com/callback?token=abc&state=123",
            status_code=200,
            history=[],
        )
        v._validate_response(r)  # Должен пройти

    def test_redirect_whitelist_dict_missing_param_fails(self):
        """Dict-паттерн: параметр отсутствует → не совпало → RedirectError."""
        from request_pydantic_decorator import RedirectError
        v = ResponseValidator().redirect().whitelist([{"token": "abc"}])
        r = FakeResponse(
            url="https://example.com/callback?state=123",
            status_code=200,
            history=[],
        )
        with pytest.raises(RedirectError):
            v._validate_response(r)
