# -*- coding: utf-8 -*-
"""
Стресс-тесты и поиск багов: массовые async-вызовы, пограничные случаи,
невалидные данные, повторное использование валидатора, потокобезопасность.
"""
import asyncio
import logging
from concurrent.futures import ThreadPoolExecutor

import pytest
from pydantic import BaseModel

from request_pydantic_decorator import (
    validate_response,
    request_logging,
    ResponseValidator,
    status_code_is,
    Exceptions,
    HttpResponse,
    HeaderError,
)
from conftest import FakeResponse, make_response


# ---------- Массовые async-вызовы ----------


class TestConcurrentAsync:
    @pytest.mark.asyncio
    async def test_many_concurrent_async_validate_response(self):
        validator = ResponseValidator().status_code().whitelist([200, 201])
        call_count = 0

        @validate_response(validator=validator)
        async def fetch(i: int):
            nonlocal call_count
            call_count += 1
            return make_response(status_code=200 if i % 2 == 0 else 201, url=f"https://example.com/{i}")

        tasks = [fetch(i) for i in range(100)]
        results = await asyncio.gather(*tasks)
        assert len(results) == 100
        assert all(r.error is None for r in results)
        assert call_count == 100

    @pytest.mark.asyncio
    async def test_concurrent_mixed_success_and_fail_raise_exceptions_false(self):
        validator = ResponseValidator().status_code().whitelist([200])
        results_ok = []
        results_fail = []

        @validate_response(validator=validator, raise_exceptions=False)
        async def fetch(status: int):
            return make_response(status_code=status)

        tasks_ok = [fetch(200) for _ in range(50)]
        tasks_fail = [fetch(500) for _ in range(50)]
        out_ok = await asyncio.gather(*tasks_ok)
        out_fail = await asyncio.gather(*tasks_fail)
        assert all(r.error is None for r in out_ok)
        assert all(r.error is not None for r in out_fail)
        assert all(r.response.status_code == 500 for r in out_fail)

    @pytest.mark.asyncio
    async def test_same_validator_instance_reused_concurrent(self):
        validator = ResponseValidator().status_code().whitelist([200])

        @validate_response(validator=validator)
        async def get_200():
            return make_response(status_code=200)

        tasks = [get_200() for _ in range(200)]
        results = await asyncio.gather(*tasks)
        assert len(results) == 200
        assert all(r.error is None for r in results)

    @pytest.mark.asyncio
    async def test_stress_async_with_model_and_validator(self):
        class M(BaseModel):
            x: int

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

        @validate_response(model=M, body_type="json", validator=validator)
        async def get_json(i: int):
            return make_response(status_code=200, json_body={"x": i})

        tasks = [get_json(i) for i in range(150)]
        results = await asyncio.gather(*tasks)
        assert len(results) == 150
        for i, r in enumerate(results):
            assert r.validate is not None
            assert r.validate.x == i


# ---------- Пограничные случаи валидаторов ----------


class TestEdgeCasesValidators:
    def test_empty_headers_dict(self):
        v = ResponseValidator().headers_key().whitelist([])
        r = make_response(headers={})
        v._validate_response(r)

    def test_headers_value_validator_header_missing_no_raise(self):
        v = (
            ResponseValidator()
            .headers_value()
            .for_header("X-Missing")
            .whitelist(["yes"])
        )
        r = make_response(headers={"Content-Type": "application/json"})
        v._validate_response(r)

    def test_redirect_empty_history(self):
        v = ResponseValidator().redirect().whitelist(["example.com"])
        r = FakeResponse(url="https://example.com/", history=[])
        v._validate_response(r)

    def test_status_code_zero_whitelist(self):
        v = ResponseValidator().status_code().whitelist([0])
        r = make_response(status_code=0)
        v._validate_response(r)

    def test_when_all_one_condition(self):
        v = ResponseValidator().when_all(status_code_is(418), raise_as=ValueError, message="Teapot")
        r = make_response(status_code=418)
        with pytest.raises(ValueError):
            v._validate_response(r)


# ---------- Невалидные / опасные данные ----------


class TestInvalidData:
    def test_response_json_returns_non_dict(self):
        @validate_response(model=type("M", (BaseModel,), {"__annotations__": {"x": str}}), body_type="json")
        def get():
            return make_response(status_code=200, json_body="not a dict")

        with pytest.raises(Exception):
            get()

    def test_response_json_raises_on_invalid_json(self):
        class M(BaseModel):
            x: int

        class BadJsonResponse(FakeResponse):
            """Response с json(), который бросает."""
            def json(self, **kwargs):
                raise ValueError("Invalid JSON")

        resp = BadJsonResponse(status_code=200, text="not json", json_obj=None)

        @validate_response(model=M, body_type="json")
        def get():
            return resp

        with pytest.raises(ValueError, match="Invalid JSON"):
            get()

    def test_validate_response_raise_exceptions_false_captures_validation_error(self):
        class M(BaseModel):
            x: int

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

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

    def test_validate_response_raise_exceptions_false_captures_status_code_error(self):
        validator = ResponseValidator().status_code().whitelist([200])
        @validate_response(validator=validator, raise_exceptions=False)
        def get():
            return make_response(status_code=999)

        result = get()
        assert result.error is not None
        assert isinstance(result.error, Exceptions.Validation.StatusCode)


# ---------- Проверка присвоения response исключению ----------


class TestExceptionResponseAttribute:
    def test_captured_error_has_response(self):
        validator = ResponseValidator().status_code().whitelist([200])
        @validate_response(validator=validator, raise_exceptions=False)
        def get():
            return make_response(status_code=500, url="https://broken.api/")

        result = get()
        assert result.error is not None
        assert getattr(result.error, "response", None) is not None
        assert result.error.response.status_code == 500

    def test_raised_exception_has_response(self):
        validator = ResponseValidator().status_code().whitelist([200])
        @validate_response(validator=validator)
        def get():
            return make_response(status_code=403)

        try:
            get()
        except Exception as e:
            assert getattr(e, "response", None) is not None


# ---------- Декоратор без validator и без model ----------


class TestNoValidatorNoModel:
    async def test_async_return_validate_response_structure(self):
        @validate_response()
        async def get():
            return make_response(status_code=201, text="created")

        result = await get()
        assert hasattr(result, "response")
        assert hasattr(result, "validate")
        assert hasattr(result, "error")
        assert result.validate is None
        assert result.error is None


# ---------- request_logging не ломает возврат значения ----------


class TestRequestLoggingPreservesReturn:
    def test_sync_returns_same_object(self):
        @request_logging(save_errors_to_file=False)
        def get():
            return make_response(status_code=200)

        r = get()
        assert r.status_code == 200

    async def test_async_returns_same_object(self):
        @request_logging(save_errors_to_file=False)
        async def get():
            return make_response(status_code=200)

        r = await get()
        assert r.status_code == 200

    async def test_logging_plus_validate_async_returns_validate_response(self):
        @request_logging(save_errors_to_file=False)
        @validate_response(validator=ResponseValidator().status_code().whitelist([200]))
        async def get():
            return make_response(status_code=200)

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


# ---------- Потенциальные баги: нестандартные типы ----------


class TestNonStringHeaderValue:
    """После фикса: не-строковые значения приводятся к str перед .lower()."""

    def test_header_value_non_string_coerced_to_str(self):
        v = (
            ResponseValidator()
            .headers_value()
            .for_header("X-Num")
            .whitelist(["123"])
        )
        r = make_response(headers={"X-Num": 123})
        # После фикса: str(123) → "123", в whitelist → проходит
        v._validate_response(r)

    def test_header_value_non_string_fails_whitelist(self):
        v = (
            ResponseValidator()
            .headers_value()
            .for_header("X-Num")
            .whitelist(["1"])
        )
        r = make_response(headers={"X-Num": 123})
        # str(123) → "123", не в whitelist ["1"] → HeaderError
        with pytest.raises(HeaderError):
            v._validate_response(r)


class TestResponseJsonReturnsList:
    """body_type=json и response.json() возвращает list — Pydantic может валидировать через TypeAdapter(list[Model])."""

    def test_json_returns_list_with_union_or_adapter(self):
        from pydantic import TypeAdapter
        TA = TypeAdapter(list[dict])
        @validate_response(model=TA, body_type="json")
        def get():
            return make_response(status_code=200, json_body=[{"a": 1}, {"a": 2}])
        result = get()
        assert result.validate == [{"a": 1}, {"a": 2}]


# ---------- HttpResponse protocol: минимальный объект ----------


class TestHttpResponseProtocol:
    def test_minimal_protocol_implementer(self):
        class MinimalResp:
            status_code = 200
            headers = {}
            url = "https://x.org/"
            history = []
            text = ""
            cookies = {}

            def json(self, **kwargs):
                return {}

            @property
            def request(self):
                return None

        v = ResponseValidator().status_code().whitelist([200])
        assert isinstance(MinimalResp(), HttpResponse)
        v._validate_response(MinimalResp())
