# -*- coding: utf-8 -*-
"""Тесты декораторов validate_response и request_logging: sync/async, model, validator, raise_exceptions."""
import asyncio
import logging
import tempfile
from pathlib import Path

import pytest
from pydantic import BaseModel, TypeAdapter

from request_pydantic_decorator import (
    validate_response,
    request_logging,
    ResponseValidator,
    Exceptions,
    ValidateResponse,
)
from conftest import FakeResponse, make_response


# ---------- Модели для JSON-валидации ----------


class SimpleModel(BaseModel):
    value: int
    name: str


class NestedModel(BaseModel):
    id: int
    data: SimpleModel


# ---------- validate_response: без model, без validator ----------


class TestValidateResponseBare:
    def test_sync_no_model_no_validator_returns_validate_response(self):
        @validate_response()
        def get_sync():
            return make_response(status_code=200, text="ok")

        result = get_sync()
        assert isinstance(result, ValidateResponse)
        assert result.error is None
        assert result.validate is None
        assert result.response.status_code == 200

    async def test_async_no_model_no_validator(self):
        @validate_response()
        async def get_async():
            return make_response(status_code=200, text="ok")

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

    def test_sync_non_http_response_raises_type_error(self):
        @validate_response()
        def bad():
            return "not a response"

        with pytest.raises(TypeError) as exc_info:
            bad()
        assert "HttpResponse" in str(exc_info.value) or "протокол" in str(exc_info.value).lower()


# ---------- validate_response: с validator ----------


class TestValidateResponseWithValidator:
    def test_sync_validator_pass(self):
        validator = ResponseValidator().status_code().whitelist([200])
        @validate_response(validator=validator)
        def get_sync():
            return make_response(status_code=200)

        result = get_sync()
        assert result.error is None

    def test_sync_validator_fail_raises(self):
        validator = ResponseValidator().status_code().whitelist([200])
        @validate_response(validator=validator)
        def get_sync():
            return make_response(status_code=500)

        with pytest.raises(Exceptions.Validation.StatusCode):
            get_sync()

    def test_sync_validator_fail_raise_exceptions_false(self):
        validator = ResponseValidator().status_code().whitelist([200])
        @validate_response(validator=validator, raise_exceptions=False)
        def get_sync():
            return make_response(status_code=500)

        result = get_sync()
        assert result.error is not None
        assert isinstance(result.error, Exceptions.Validation.StatusCode)
        assert result.validate is None
        assert result.response.status_code == 500

    async def test_async_validator_fail_raise_exceptions_false(self):
        validator = ResponseValidator().status_code().whitelist([200])
        @validate_response(validator=validator, raise_exceptions=False)
        async def get_async():
            return make_response(status_code=403)

        result = await get_async()
        assert result.error is not None
        assert result.response.status_code == 403

    def test_exception_has_response_attr_when_raised(self):
        validator = ResponseValidator().status_code().whitelist([200])
        @validate_response(validator=validator)
        def get_sync():
            return make_response(status_code=500, url="https://api.test/fail")

        try:
            get_sync()
        except Exception as e:
            assert getattr(e, "response", None) is not None
            assert e.response.status_code == 500
            assert "api.test" in str(e.response.url)


# ---------- validate_response: с model (JSON) ----------


class TestValidateResponseWithModel:
    def test_sync_model_json_pass(self):
        @validate_response(model=SimpleModel, body_type="json")
        def get_sync():
            return make_response(status_code=200, json_body={"value": 42, "name": "test"})

        result = get_sync()
        assert result.error is None
        assert result.validate is not None
        assert result.validate.value == 42
        assert result.validate.name == "test"

    def test_sync_model_json_fail_validation_raises(self):
        @validate_response(model=SimpleModel, body_type="json")
        def get_sync():
            return make_response(status_code=200, json_body={"value": "not_int", "name": "x"})

        with pytest.raises(Exception):
            get_sync()

    def test_sync_model_json_fail_raise_exceptions_false(self):
        @validate_response(model=SimpleModel, body_type="json", raise_exceptions=False)
        def get_sync():
            return make_response(status_code=200, json_body={"wrong": "shape"})

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

    async def test_async_model_json_pass(self):
        @validate_response(model=SimpleModel, body_type="json")
        async def get_async():
            return make_response(status_code=200, json_body={"value": 1, "name": "a"})

        result = await get_async()
        assert result.validate is not None
        assert result.validate.value == 1

    def test_model_without_body_type_raises(self):
        with pytest.raises(TypeError, match="body_type"):
            @validate_response(model=SimpleModel)
            def f():
                return make_response()

    def test_model_and_validator_together(self):
        validator = ResponseValidator().status_code().whitelist([200])
        @validate_response(model=SimpleModel, body_type="json", validator=validator)
        def get_sync():
            return make_response(status_code=200, json_body={"value": 10, "name": "n"})

        result = get_sync()
        assert result.validate is not None
        assert result.validate.value == 10


# ---------- validate_response: body_type html ----------


class TestValidateResponseBodyTypeHtml:
    def test_body_type_html_uses_text(self):
        @validate_response(model=TypeAdapter(str), body_type="html")
        def get_sync():
            return make_response(status_code=200, text="<html>ok</html>", json_body=None)

        result = get_sync()
        assert result.validate == "<html>ok</html>"


# ---------- request_logging ----------


class TestRequestLogging:
    def test_sync_success_logs(self, caplog):
        with caplog.at_level(logging.INFO):
            @request_logging(save_errors_to_file=False)
            def get_sync():
                return make_response(status_code=200, url="https://example.com/")

            get_sync()
        assert any("example.com" in rec.message for rec in caplog.records)

    def test_sync_error_logs_and_reraises(self, caplog):
        with caplog.at_level(logging.ERROR):
            @request_logging(save_errors_to_file=False)
            def get_sync():
                resp = make_response(status_code=500)
                raise RuntimeError("Сервер вернул 500")

            with pytest.raises(RuntimeError):
                get_sync()
        assert any(rec.levelname == "ERROR" for rec in caplog.records)

    async def test_async_success_logs(self, caplog):
        with caplog.at_level(logging.INFO):
            @request_logging(save_errors_to_file=False)
            async def get_async():
                return make_response(status_code=200)

            await get_async()
        assert len(caplog.records) >= 1

    def test_save_errors_to_file_creates_json(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            validator = ResponseValidator().status_code().whitelist([200])
            @request_logging(save_errors_to_file=True, errors_output_dir=tmpdir)
            @validate_response(validator=validator)
            def fail():
                return make_response(status_code=500, url="https://test.example/err")

            with pytest.raises(Exception):
                fail()
            files = list(Path(tmpdir).rglob("*.json"))
            assert len(files) >= 1
            content = files[0].read_text(encoding="utf-8")
            assert "500" in content or "StatusCodeError" in content or "exception" in content.lower()


# ---------- Порядок декораторов и комбинирование ----------


class TestDecoratorOrder:
    async def test_validate_then_logging_async(self, caplog):
        validator = ResponseValidator().status_code().whitelist([200])
        @request_logging(save_errors_to_file=False)
        @validate_response(validator=validator)
        async def get_async():
            return make_response(status_code=200)

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

    async def test_validate_fail_logging_still_logs_error(self, caplog):
        validator = ResponseValidator().status_code().whitelist([200])
        with caplog.at_level(logging.ERROR):
            @request_logging(save_errors_to_file=False)
            @validate_response(validator=validator)
            async def get_async():
                return make_response(status_code=403)

            with pytest.raises(Exception):
                await get_async()
        assert any(rec.levelname == "ERROR" for rec in caplog.records)


# ---------- Исключение до возврата response (сетевая ошибка и т.д.) ----------


class TestExceptionBeforeResponse:
    async def test_async_raises_before_return_response_is_none(self):
        @validate_response()
        async def get_async():
            raise ValueError("network error")

        with pytest.raises(ValueError, match="network error"):
            await get_async()

    def test_exception_gets_response_none_when_raised_inside_func(self):
        @validate_response()
        def get_sync():
            raise RuntimeError("connection failed")

        with pytest.raises(RuntimeError):
            get_sync()
