from __future__ import annotations

import sys
import warnings
from typing import TYPE_CHECKING, Any

import pytest
from twisted.internet.defer import Deferred

from scrapy.exceptions import ScrapyDeprecationWarning
from scrapy.utils.decorators import _warn_spider_arg, deprecated, inthread
from scrapy.utils.defer import maybe_deferred_to_future
from tests.utils.decorators import coroutine_test

if TYPE_CHECKING:
    from collections.abc import AsyncGenerator, Callable


class TestDeprecated:
    def test_warns_and_still_calls(self):
        @deprecated()
        def add(a: int, b: int) -> int:
            return a + b

        with pytest.warns(
            ScrapyDeprecationWarning, match=r"Call to deprecated function add\."
        ):
            result = add(2, 3)

        assert result == 5

    def test_use_instead_in_message(self):
        @deprecated(use_instead="other_function")
        def old() -> None:
            return None

        with pytest.warns(
            ScrapyDeprecationWarning,
            match=r"Call to deprecated function old\. Use other_function instead\.",
        ):
            old()

    def test_applied_without_parentheses(self):
        @deprecated
        def square(x: int) -> int:
            return x * x

        with pytest.warns(
            ScrapyDeprecationWarning, match=r"Call to deprecated function square\."
        ) as record:
            result = square(4)

        assert result == 16
        # No "Use ... instead." part when applied directly to the function.
        assert "instead" not in str(record[0].message)


class TestInthread:
    @coroutine_test
    async def test_returns_deferred_with_result(self):
        @inthread
        def multiply(a, b):
            return a * b

        deferred = multiply(6, 7)
        assert isinstance(deferred, Deferred)
        assert await maybe_deferred_to_future(deferred) == 42


class TestWarnSpiderArg:
    def test_sync_warns_with_spider_arg(self):
        @_warn_spider_arg
        def parse(response: str, spider: str | None = None) -> str:
            return response

        with pytest.warns(
            ScrapyDeprecationWarning, match=r"Passing a 'spider' argument"
        ):
            assert parse("response", spider="spider") == "response"

    @pytest.mark.skipif(
        sys.version_info < (3, 14),
        reason="annotations are only lazily evaluated since Python 3.14 (PEP 649)",
    )
    def test_sync_warns_with_unresolvable_annotations(self):
        # dont_inherit=True, or the module's future import stringizes the annotations
        namespace: dict[str, Any] = {}
        exec(  # pylint: disable=exec-used
            compile(
                "def parse(response: OnlyAtTypeCheckingTime,"
                " spider: OnlyAtTypeCheckingTime | None = None): return response",
                "<test>",
                "exec",
                dont_inherit=True,
            ),
            namespace,
        )
        parse_func: Callable[..., str] = namespace["parse"]
        parse = _warn_spider_arg(parse_func)

        with pytest.warns(
            ScrapyDeprecationWarning, match=r"Passing a 'spider' argument"
        ):
            assert parse("response", spider="spider") == "response"

    def test_sync_warns_with_positional_spider_arg(self):
        @_warn_spider_arg
        def parse(response: str, spider: str | None = None) -> str:
            return response

        with pytest.warns(
            ScrapyDeprecationWarning, match=r"Passing a 'spider' argument"
        ):
            assert parse("response", "spider") == "response"

    def test_sync_warns_with_keyword_only_spider_arg(self):
        @_warn_spider_arg
        def parse(response: str, *, spider: str | None = None) -> str:
            return response

        with pytest.warns(
            ScrapyDeprecationWarning, match=r"Passing a 'spider' argument"
        ):
            assert parse("response", spider="spider") == "response"

    def test_sync_no_warning_without_spider_arg(self):
        @_warn_spider_arg
        def parse(response: str, spider: str | None = None) -> str:
            return response

        with warnings.catch_warnings():
            warnings.simplefilter("error", category=ScrapyDeprecationWarning)
            assert parse("response") == "response"

    @coroutine_test
    async def test_async_warns_with_spider_arg(self):
        @_warn_spider_arg
        async def parse(response: str, spider: str | None = None) -> str:
            return response

        with pytest.warns(
            ScrapyDeprecationWarning, match=r"Passing a 'spider' argument"
        ):
            assert await parse("response", spider="spider") == "response"

    @coroutine_test
    async def test_asyncgen_warns_with_spider_arg(self):
        @_warn_spider_arg
        async def parse(
            response: str, spider: str | None = None
        ) -> AsyncGenerator[str]:
            yield response

        with pytest.warns(
            ScrapyDeprecationWarning, match=r"Passing a 'spider' argument"
        ):
            results = [item async for item in parse("response", spider="spider")]

        assert results == ["response"]
