from __future__ import annotations

import asyncio
import inspect
import platform
import sys
from typing import TYPE_CHECKING

import pytest

from tests.spiders import ExceptionSpider, NoRequestsSpider
from tests.utils.cmdline import proc

if TYPE_CHECKING:
    from collections.abc import Iterable
    from pathlib import Path


# Header for spider classes copied with inspect.getsource(): the future import
# keeps their annotations from being evaluated in the generated module.
SPIDER_HEADER = "from __future__ import annotations\nfrom scrapy import Spider\n"


class TestRunSpiderCommand:
    spider_filename = "myspider.py"

    debug_log_spider = """
import scrapy

class MySpider(scrapy.Spider):
    name = 'myspider'

    async def start(self):
        self.logger.debug("It Works!")
        return
        yield
"""

    badspider = """
import scrapy

class BadSpider(scrapy.Spider):
    name = "bad"
    async def start(self):
        raise Exception("oops!")
        yield
        """

    def runspider(
        self, cwd: Path, code: str, name: str | None = None, args: Iterable[str] = ()
    ) -> tuple[int, str, str]:
        fname = cwd / (name or self.spider_filename)
        fname.write_text(code, encoding="utf-8")
        return proc("runspider", str(fname), *args, cwd=cwd)

    def get_log(
        self, cwd: Path, code: str, name: str | None = None, args: Iterable[str] = ()
    ) -> str:
        _, _, stderr = self.runspider(cwd, code, name, args=args)
        return stderr

    def test_runspider(self, tmp_path: Path) -> None:
        log = self.get_log(tmp_path, self.debug_log_spider)
        assert "DEBUG: It Works!" in log
        assert (
            "Using reactor: twisted.internet.asyncioreactor.AsyncioSelectorReactor"
            in log
        )
        assert "INFO: Spider closed (finished)" in log

    def test_run_fail_spider(self, tmp_path: Path) -> None:
        ret, _, _ = self.runspider(
            tmp_path, SPIDER_HEADER + inspect.getsource(ExceptionSpider)
        )
        assert ret != 0

    def test_run_good_spider(self, tmp_path: Path) -> None:
        ret, _, _ = self.runspider(
            tmp_path,
            SPIDER_HEADER + inspect.getsource(NoRequestsSpider),
        )
        assert ret == 0

    def test_runspider_log_level(self, tmp_path: Path) -> None:
        log = self.get_log(
            tmp_path, self.debug_log_spider, args=("-s", "LOG_LEVEL=INFO")
        )
        assert "DEBUG: It Works!" not in log
        assert "INFO: Spider opened" in log

    def test_runspider_default_reactor(self, tmp_path: Path) -> None:
        log = self.get_log(
            tmp_path, self.debug_log_spider, args=("-s", "TWISTED_REACTOR=")
        )
        assert "DEBUG: It Works!" in log
        assert (
            "Using reactor: twisted.internet.asyncioreactor.AsyncioSelectorReactor"
            not in log
        )
        assert "INFO: Spider opened" in log
        assert "INFO: Closing spider (finished)" in log
        assert "INFO: Spider closed (finished)" in log

    def test_runspider_dnscache_disabled(self, tmp_path: Path) -> None:
        # see https://github.com/scrapy/scrapy/issues/2811
        # The spider below should not be able to connect to localhost:12345,
        # which is intended,
        # but this should not be because of DNS lookup error
        # assumption: localhost will resolve in all cases (true?)
        dnscache_spider = """
import scrapy

class MySpider(scrapy.Spider):
    name = 'myspider'
    start_urls = ['http://localhost:12345']

    custom_settings = {
        "ROBOTSTXT_OBEY": False,
        "RETRY_ENABLED": False,
    }

    def parse(self, response):
        return {'test': 'value'}
"""
        log = self.get_log(
            tmp_path, dnscache_spider, args=("-s", "DNSCACHE_ENABLED=False")
        )
        assert "CannotResolveHostError" not in log
        assert "INFO: Spider opened" in log

    @pytest.mark.parametrize("value", [False, True])
    def test_runspider_log_short_names(self, tmp_path: Path, value: bool) -> None:
        log1 = self.get_log(
            tmp_path, self.debug_log_spider, args=("-s", f"LOG_SHORT_NAMES={value}")
        )
        assert "[myspider] DEBUG: It Works!" in log1
        assert ("[scrapy]" in log1) is value
        assert ("[scrapy.core.engine]" in log1) is not value

    def test_runspider_no_spider_found(self, tmp_path: Path) -> None:
        log = self.get_log(tmp_path, "from scrapy.spiders import Spider\n")
        assert "No spider found in file" in log

    @pytest.mark.parametrize("args", [(), ("a.py", "b.py")])
    def test_runspider_bad_arguments(self, args: tuple[str, ...]) -> None:
        returncode, out, _ = proc("runspider", *args)
        assert returncode == 2
        assert "Usage" in out

    def test_runspider_file_not_found(self) -> None:
        _, _, log = proc("runspider", "some_non_existent_file")
        assert "File not found: some_non_existent_file" in log

    def test_runspider_unable_to_load(self, tmp_path: Path) -> None:
        log = self.get_log(tmp_path, "", name="myspider.txt")
        assert "Unable to load" in log

    def test_start_errors(self, tmp_path: Path) -> None:
        log = self.get_log(tmp_path, self.badspider, name="badspider.py")
        assert "start" in log
        assert "badspider.py" in log, log

    def test_asyncio_enabled_true(self, tmp_path: Path) -> None:
        log = self.get_log(
            tmp_path,
            self.debug_log_spider,
            args=[
                "-s",
                "TWISTED_REACTOR=twisted.internet.asyncioreactor.AsyncioSelectorReactor",
            ],
        )
        assert (
            "Using reactor: twisted.internet.asyncioreactor.AsyncioSelectorReactor"
            in log
        )

    def test_asyncio_enabled_default(self, tmp_path: Path) -> None:
        log = self.get_log(tmp_path, self.debug_log_spider)
        assert (
            "Using reactor: twisted.internet.asyncioreactor.AsyncioSelectorReactor"
            in log
        )

    def test_asyncio_enabled_false(self, tmp_path: Path) -> None:
        log = self.get_log(
            tmp_path,
            self.debug_log_spider,
            args=["-s", "TWISTED_REACTOR=twisted.internet.selectreactor.SelectReactor"],
        )
        assert "Using reactor: twisted.internet.selectreactor.SelectReactor" in log
        assert (
            "Using reactor: twisted.internet.asyncioreactor.AsyncioSelectorReactor"
            not in log
        )

    @pytest.mark.requires_uvloop
    def test_custom_asyncio_loop_enabled_true(self, tmp_path: Path) -> None:
        log = self.get_log(
            tmp_path,
            self.debug_log_spider,
            args=[
                "-s",
                "TWISTED_REACTOR=twisted.internet.asyncioreactor.AsyncioSelectorReactor",
                "-s",
                "ASYNCIO_EVENT_LOOP=uvloop.Loop",
            ],
        )
        assert "Using asyncio event loop: uvloop.Loop" in log

    def test_custom_asyncio_loop_enabled_false(self, tmp_path: Path) -> None:
        log = self.get_log(
            tmp_path,
            self.debug_log_spider,
            args=[
                "-s",
                "TWISTED_REACTOR=twisted.internet.asyncioreactor.AsyncioSelectorReactor",
            ],
        )
        if sys.platform != "win32":
            loop = asyncio.new_event_loop()
        else:
            loop = asyncio.SelectorEventLoop()
        assert (
            f"Using asyncio event loop: {loop.__module__}.{loop.__class__.__name__}"
            in log
        )
        loop.close()

    def test_no_reactor(self, tmp_path: Path) -> None:
        log = self.get_log(
            tmp_path,
            self.debug_log_spider,
            args=[
                "-s",
                "TWISTED_REACTOR_ENABLED=False",
            ],
        )
        assert "Not using a Twisted reactor" in log

    def test_output(self, tmp_path: Path) -> None:
        spider_code = """
import scrapy

class MySpider(scrapy.Spider):
    name = 'myspider'

    async def start(self):
        self.logger.debug('FEEDS: {}'.format(self.settings.getdict('FEEDS')))
        return
        yield
"""
        args = ["-o", "example.json"]
        log = self.get_log(tmp_path, spider_code, args=args)
        assert "[myspider] DEBUG: FEEDS: {'example.json': {'format': 'json'}}" in log

    def test_overwrite_output(self, tmp_path: Path) -> None:
        spider_code = """
import json
import scrapy

class MySpider(scrapy.Spider):
    name = 'myspider'

    async def start(self):
        self.logger.debug(
            'FEEDS: {}'.format(
                json.dumps(self.settings.getdict('FEEDS'), sort_keys=True)
            )
        )
        return
        yield
"""
        (tmp_path / "example.json").write_text("not empty", encoding="utf-8")
        args = ["-O", "example.json"]
        log = self.get_log(tmp_path, spider_code, args=args)
        assert (
            '[myspider] DEBUG: FEEDS: {"example.json": {"format": "json", "overwrite": true}}'
            in log
        )
        with (tmp_path / "example.json").open(encoding="utf-8") as f2:
            first_line = f2.readline()
        assert first_line != "not empty"

    def test_output_and_overwrite_output(self, tmp_path: Path) -> None:
        spider_code = """
import scrapy

class MySpider(scrapy.Spider):
    name = 'myspider'

    async def start(self):
        return
        yield
"""
        args = ["-o", "example1.json", "-O", "example2.json"]
        log = self.get_log(tmp_path, spider_code, args=args)
        assert (
            "error: Please use only one of -o/--output and -O/--overwrite-output" in log
        )

    def test_output_stdout(self, tmp_path: Path) -> None:
        spider_code = """
import scrapy

class MySpider(scrapy.Spider):
    name = 'myspider'

    async def start(self):
        self.logger.debug('FEEDS: {}'.format(self.settings.getdict('FEEDS')))
        return
        yield
"""
        args = ["-o", "-:json"]
        log = self.get_log(tmp_path, spider_code, args=args)
        assert "[myspider] DEBUG: FEEDS: {'stdout:': {'format': 'json'}}" in log

    def test_output_feed_exporter_disabled(self, tmp_path: Path) -> None:
        spider_code = """
import scrapy

class MySpider(scrapy.Spider):
    name = 'myspider'

    custom_settings = {
        "EXTENSIONS": {"scrapy.extensions.feedexport.FeedExporter": None},
    }

    start_urls = ["data:,"]

    def parse(self, response):
        yield {"hello": "world"}
"""
        args = ["-o", "example.json"]
        log = self.get_log(tmp_path, spider_code, args=args)
        assert "FeedExporter extension is not enabled" in log
        assert not (tmp_path / "example.json").exists()

    def test_output_feed_exporter_subclass(self, tmp_path: Path) -> None:
        spider_code = """
import scrapy
from scrapy.extensions.feedexport import FeedExporter

class MyFeedExporter(FeedExporter):
    pass

class MySpider(scrapy.Spider):
    name = 'myspider'

    custom_settings = {
        "EXTENSIONS": {
            "scrapy.extensions.feedexport.FeedExporter": None,
            MyFeedExporter: 0,
        },
    }

    start_urls = ["data:,"]

    def parse(self, response):
        yield {"hello": "world"}
"""
        args = ["-o", "example.json"]
        log = self.get_log(tmp_path, spider_code, args=args)
        assert "FeedExporter extension is not enabled" not in log
        assert (tmp_path / "example.json").exists()

    @pytest.mark.parametrize("arg", ["output.json:json", "output.json"])
    def test_absolute_path(self, tmp_path: Path, arg: str) -> None:
        spider_code = """
import scrapy

class MySpider(scrapy.Spider):
    name = 'myspider'

    start_urls = ["data:,"]

    def parse(self, response):
        yield {"hello": "world"}
        """

        args = ["-o", str(tmp_path / arg)]
        log = self.get_log(tmp_path, spider_code, args=args)
        assert (
            f"[scrapy.extensions.feedexport] INFO: Stored json feed (1 items) in: {tmp_path / 'output.json'}"
            in log
        )

    def test_args_change_settings(self, tmp_path: Path) -> None:
        spider_code = """
import scrapy

class MySpider(scrapy.Spider):
    name = 'myspider'

    @classmethod
    def from_crawler(cls, crawler, *args, **kwargs):
        spider = super().from_crawler(crawler, *args, **kwargs)
        spider.settings.set("FOO", kwargs.get("foo"))
        return spider

    async def start(self):
        self.logger.info(f"The value of FOO is {self.settings.getint('FOO')}")
        return
        yield
"""
        args = ["-a", "foo=42"]
        log = self.get_log(tmp_path, spider_code, args=args)
        assert "Spider closed (finished)" in log
        assert "The value of FOO is 42" in log


@pytest.mark.skipif(
    platform.system() != "Windows", reason="Windows required for .pyw files"
)
class TestWindowsRunSpiderCommand(TestRunSpiderCommand):
    spider_filename = "myspider.pyw"

    def test_start_errors(self, tmp_path: Path) -> None:
        log = self.get_log(tmp_path, self.badspider, name="badspider.pyw")
        assert "start" in log
        assert "badspider.pyw" in log

    def test_runspider_unable_to_load(self, tmp_path: Path) -> None:
        pytest.skip("Already Tested in 'RunSpiderCommandTest'")
