from __future__ import annotations

import argparse
import re
from typing import TYPE_CHECKING

import pytest

from scrapy.commands import parse
from scrapy.settings import Settings
from tests.test_commands import TestProjectBase
from tests.utils.cmdline import call, proc

if TYPE_CHECKING:
    from pathlib import Path

    from tests.mockserver.http import MockServer


class TestParseCommand(TestProjectBase):
    spider_name = "parse_spider"

    @pytest.fixture(autouse=True)
    def create_files(self, proj_path: Path) -> None:
        proj_mod_path = proj_path / self.project_name
        (proj_mod_path / "spiders" / "myspider.py").write_text(
            f"""
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from scrapy.utils.test import get_from_asyncio_queue
import asyncio


class BaseSpider(scrapy.Spider):
    custom_settings = {{
        "DOWNLOAD_DELAY": 0,
    }}


class AsyncDefAsyncioReturnSpider(BaseSpider):
    name = "asyncdef_asyncio_return"

    async def parse(self, response):
        await asyncio.sleep(0.2)
        status = await get_from_asyncio_queue(response.status)
        self.logger.info(f"Got response {{status}}")
        return [{{'id': 1}}, {{'id': 2}}]

class AsyncDefAsyncioReturnSingleElementSpider(BaseSpider):
    name = "asyncdef_asyncio_return_single_element"

    async def parse(self, response):
        await asyncio.sleep(0.1)
        status = await get_from_asyncio_queue(response.status)
        self.logger.info(f"Got response {{status}}")
        return {{'foo': 42}}

class AsyncDefAsyncioGenLoopSpider(BaseSpider):
    name = "asyncdef_asyncio_gen_loop"

    async def parse(self, response):
        for i in range(10):
            await asyncio.sleep(0.1)
            yield {{'foo': i}}
        self.logger.info(f"Got response {{response.status}}")

class AsyncDefAsyncioSpider(BaseSpider):
    name = "asyncdef_asyncio"

    async def parse(self, response):
        await asyncio.sleep(0.2)
        status = await get_from_asyncio_queue(response.status)
        self.logger.debug(f"Got response {{status}}")

class AsyncDefAsyncioGenExcSpider(BaseSpider):
    name = "asyncdef_asyncio_gen_exc"

    async def parse(self, response):
        for i in range(10):
            await asyncio.sleep(0.1)
            yield {{'foo': i}}
            if i > 5:
                raise ValueError("Stopping the processing")

class CallbackSignatureDownloaderMiddleware:
    def process_request(self, request, spider):
        from inspect import signature
        spider.logger.debug(f"request.callback signature: {{signature(request.callback)}}")


class MySpider(scrapy.Spider):
    name = '{self.spider_name}'

    custom_settings = {{
        "DOWNLOADER_MIDDLEWARES": {{
            CallbackSignatureDownloaderMiddleware: 0,
        }},
        "DOWNLOAD_DELAY": 0,
    }}

    def parse(self, response):
        if getattr(self, 'test_arg', None):
            self.logger.debug('It Works!')
        return [scrapy.Item(), dict(foo='bar')]

    def parse_request_with_meta(self, response):
        foo = response.meta.get('foo', 'bar')

        if foo == 'bar':
            self.logger.debug('It Does Not Work :(')
        else:
            self.logger.debug('It Works!')

    def parse_request_with_cb_kwargs(self, response, foo=None, key=None):
        if foo == 'bar' and key == 'value':
            self.logger.debug('It Works!')
        else:
            self.logger.debug('It Does Not Work :(')

    def parse_request_without_meta(self, response):
        foo = response.meta.get('foo', 'bar')

        if foo == 'bar':
            self.logger.debug('It Works!')
        else:
            self.logger.debug('It Does Not Work :(')

class MyGoodCrawlSpider(CrawlSpider):
    name = 'goodcrawl{self.spider_name}'

    custom_settings = {{
        "DOWNLOAD_DELAY": 0,
    }}

    rules = (
        Rule(LinkExtractor(allow=r'/html'), callback='parse_item', follow=True),
        Rule(LinkExtractor(allow=r'/text'), follow=True),
    )

    def parse_item(self, response):
        return [scrapy.Item(), dict(foo='bar')]

    def parse(self, response):
        return [scrapy.Item(), dict(nomatch='default')]


class MyBadCrawlSpider(CrawlSpider):
    '''Spider which doesn't define a parse_item callback while using it in a rule.'''
    name = 'badcrawl{self.spider_name}'

    custom_settings = {{
        "DOWNLOAD_DELAY": 0,
    }}

    rules = (
        Rule(LinkExtractor(allow=r'/html'), callback='parse_item', follow=True),
    )

    def parse(self, response):
        return [scrapy.Item(), dict(foo='bar')]
""",
            encoding="utf-8",
        )

        (proj_mod_path / "pipelines.py").write_text(
            """
import logging

class MyPipeline:
    component_name = 'my_pipeline'

    def process_item(self, item):
        logging.info('It Works!')
        return item
""",
            encoding="utf-8",
        )

        with (proj_mod_path / "settings.py").open("a", encoding="utf-8") as f:
            f.write(
                f"""
ITEM_PIPELINES = {{'{self.project_name}.pipelines.MyPipeline': 1}}
"""
            )

    def test_spider_arguments(self, proj_path: Path, mockserver: MockServer) -> None:
        _, _, stderr = proc(
            "parse",
            "--spider",
            self.spider_name,
            "-a",
            "test_arg=1",
            "-c",
            "parse",
            "--verbose",
            mockserver.url("/html"),
            cwd=proj_path,
        )
        assert "DEBUG: It Works!" in stderr

    def test_request_with_meta(self, proj_path: Path, mockserver: MockServer) -> None:
        raw_json_string = '{"foo" : "baz"}'
        _, _, stderr = proc(
            "parse",
            "--spider",
            self.spider_name,
            "--meta",
            raw_json_string,
            "-c",
            "parse_request_with_meta",
            "--verbose",
            mockserver.url("/html"),
            cwd=proj_path,
        )
        assert "DEBUG: It Works!" in stderr

        _, _, stderr = proc(
            "parse",
            "--spider",
            self.spider_name,
            "-m",
            raw_json_string,
            "-c",
            "parse_request_with_meta",
            "--verbose",
            mockserver.url("/html"),
            cwd=proj_path,
        )
        assert "DEBUG: It Works!" in stderr

    def test_request_with_cb_kwargs(
        self, proj_path: Path, mockserver: MockServer
    ) -> None:
        raw_json_string = '{"foo" : "bar", "key": "value"}'
        _, _, stderr = proc(
            "parse",
            "--spider",
            self.spider_name,
            "--cbkwargs",
            raw_json_string,
            "-c",
            "parse_request_with_cb_kwargs",
            "--verbose",
            mockserver.url("/html"),
            cwd=proj_path,
        )
        assert "DEBUG: It Works!" in stderr
        assert (
            "DEBUG: request.callback signature: (response, foo=None, key=None)"
            in stderr
        )

    def test_request_without_meta(
        self, proj_path: Path, mockserver: MockServer
    ) -> None:
        _, _, stderr = proc(
            "parse",
            "--spider",
            self.spider_name,
            "-c",
            "parse_request_without_meta",
            "--nolinks",
            mockserver.url("/html"),
            cwd=proj_path,
        )
        assert "DEBUG: It Works!" in stderr

    def test_pipelines(self, proj_path: Path, mockserver: MockServer) -> None:
        _, _, stderr = proc(
            "parse",
            "--spider",
            self.spider_name,
            "--pipelines",
            "-c",
            "parse",
            "--verbose",
            mockserver.url("/html"),
            cwd=proj_path,
        )
        assert "INFO: It Works!" in stderr

    def test_async_def_asyncio_parse_items_list(
        self, proj_path: Path, mockserver: MockServer
    ) -> None:
        _, out, stderr = proc(
            "parse",
            "--spider",
            "asyncdef_asyncio_return",
            "-c",
            "parse",
            mockserver.url("/html"),
            cwd=proj_path,
        )
        assert "INFO: Got response 200" in stderr
        assert "{'id': 1}" in out
        assert "{'id': 2}" in out

    def test_async_def_asyncio_parse_items_single_element(
        self, proj_path: Path, mockserver: MockServer
    ) -> None:
        _, out, stderr = proc(
            "parse",
            "--spider",
            "asyncdef_asyncio_return_single_element",
            "-c",
            "parse",
            mockserver.url("/html"),
            cwd=proj_path,
        )
        assert "INFO: Got response 200" in stderr
        assert "{'foo': 42}" in out

    def test_async_def_asyncgen_parse_loop(
        self, proj_path: Path, mockserver: MockServer
    ) -> None:
        _, out, stderr = proc(
            "parse",
            "--spider",
            "asyncdef_asyncio_gen_loop",
            "-c",
            "parse",
            mockserver.url("/html"),
            cwd=proj_path,
        )
        assert "INFO: Got response 200" in stderr
        for i in range(10):
            assert f"{{'foo': {i}}}" in out

    def test_async_def_asyncgen_parse_exc(
        self, proj_path: Path, mockserver: MockServer
    ) -> None:
        _, out, stderr = proc(
            "parse",
            "--spider",
            "asyncdef_asyncio_gen_exc",
            "-c",
            "parse",
            mockserver.url("/html"),
            cwd=proj_path,
        )
        assert "ValueError" in stderr
        for i in range(7):
            assert f"{{'foo': {i}}}" in out

    def test_async_def_asyncio_parse(
        self, proj_path: Path, mockserver: MockServer
    ) -> None:
        _, _, stderr = proc(
            "parse",
            "--spider",
            "asyncdef_asyncio",
            "-c",
            "parse",
            mockserver.url("/html"),
            cwd=proj_path,
        )
        assert "DEBUG: Got response 200" in stderr

    def test_parse_items(self, proj_path: Path, mockserver: MockServer) -> None:
        _, out, _ = proc(
            "parse",
            "--spider",
            self.spider_name,
            "-c",
            "parse",
            mockserver.url("/html"),
            cwd=proj_path,
        )
        assert "[{}, {'foo': 'bar'}]" in out

    def test_parse_items_no_callback_passed(
        self, proj_path: Path, mockserver: MockServer
    ) -> None:
        _, out, _ = proc(
            "parse",
            "--spider",
            self.spider_name,
            mockserver.url("/html"),
            cwd=proj_path,
        )
        assert "[{}, {'foo': 'bar'}]" in out

    def test_wrong_callback_passed(
        self, proj_path: Path, mockserver: MockServer
    ) -> None:
        _, out, stderr = proc(
            "parse",
            "--spider",
            self.spider_name,
            "-c",
            "dummy",
            mockserver.url("/html"),
            cwd=proj_path,
        )
        assert re.search(r"# Scraped Items  -+\r?\n\[\]", out)
        assert "Cannot find callback" in stderr

    def test_crawlspider_matching_rule_callback_set(
        self, proj_path: Path, mockserver: MockServer
    ) -> None:
        """If a rule matches the URL, use it's defined callback."""
        _, out, _ = proc(
            "parse",
            "--spider",
            "goodcrawl" + self.spider_name,
            "-r",
            mockserver.url("/html"),
            cwd=proj_path,
        )
        assert "[{}, {'foo': 'bar'}]" in out

    def test_crawlspider_matching_rule_default_callback(
        self, proj_path: Path, mockserver: MockServer
    ) -> None:
        """If a rule match but it has no callback set, use the 'parse' callback."""
        _, out, _ = proc(
            "parse",
            "--spider",
            "goodcrawl" + self.spider_name,
            "-r",
            mockserver.url("/text"),
            cwd=proj_path,
        )
        assert "[{}, {'nomatch': 'default'}]" in out

    def test_spider_with_no_rules_attribute(
        self, proj_path: Path, mockserver: MockServer
    ) -> None:
        """Using -r with a spider with no rule should not produce items."""
        _, out, stderr = proc(
            "parse",
            "--spider",
            self.spider_name,
            "-r",
            mockserver.url("/html"),
            cwd=proj_path,
        )
        assert re.search(r"# Scraped Items  -+\r?\n\[\]", out)
        assert "No CrawlSpider rules found" in stderr

    def test_crawlspider_missing_callback(
        self, proj_path: Path, mockserver: MockServer
    ) -> None:
        _, out, _ = proc(
            "parse",
            "--spider",
            "badcrawl" + self.spider_name,
            "-r",
            mockserver.url("/html"),
            cwd=proj_path,
        )
        assert re.search(r"# Scraped Items  -+\r?\n\[\]", out)

    def test_crawlspider_no_matching_rule(
        self, proj_path: Path, mockserver: MockServer
    ) -> None:
        """The requested URL has no matching rule, so no items should be scraped"""
        _, out, stderr = proc(
            "parse",
            "--spider",
            "badcrawl" + self.spider_name,
            "-r",
            mockserver.url("/enc-gb18030"),
            cwd=proj_path,
        )
        assert re.search(r"# Scraped Items  -+\r?\n\[\]", out)
        assert "Cannot find a rule that matches" in stderr

    def test_crawlspider_not_exists_with_not_matched_url(
        self, proj_path: Path, mockserver: MockServer
    ) -> None:
        assert call("parse", mockserver.url("/invalid_url"), cwd=proj_path) == 0

    def test_output_flag(self, proj_path: Path, mockserver: MockServer) -> None:
        """Checks if a file was created successfully having
        correct format containing correct data in it.
        """
        file_name = "data.json"
        file_path = proj_path / file_name
        proc(
            "parse",
            "--spider",
            self.spider_name,
            "-c",
            "parse",
            "-o",
            file_name,
            mockserver.url("/html"),
            cwd=proj_path,
        )

        assert file_path.exists()
        assert file_path.is_file()

        content = '[\n{},\n{"foo": "bar"}\n]'
        assert file_path.read_text(encoding="utf-8") == content

    def test_parse_add_options(self):
        command = parse.Command()
        command.settings = Settings()
        parser = argparse.ArgumentParser(
            prog="scrapy",
            formatter_class=argparse.HelpFormatter,
            conflict_handler="resolve",
            prefix_chars="-",
        )
        command.add_options(parser)
        namespace = parser.parse_args(
            ["--verbose", "--nolinks", "-d", "2", "--spider", self.spider_name]
        )
        assert namespace.nolinks
        assert namespace.depth == 2
        assert namespace.spider == self.spider_name
        assert namespace.verbose

    def test_no_reactor(self, proj_path: Path, mockserver: MockServer) -> None:
        _, out, stderr = proc(
            "parse",
            "--spider",
            "asyncdef_asyncio_return",
            "-c",
            "parse",
            mockserver.url("/html"),
            "-s",
            "TWISTED_REACTOR_ENABLED=False",
            cwd=proj_path,
        )
        assert "INFO: Got response 200" in stderr
        assert "{'id': 1}" in out
        assert "{'id': 2}" in out
