import contextlib
import shutil
import sys
from pathlib import Path
from unittest import mock

import pytest

# ugly hack to avoid cyclic imports of scrapy.spiders when running this test
# alone
import scrapy
from scrapy.crawler import CrawlerRunner
from scrapy.http import Request
from scrapy.settings import Settings
from scrapy.spiderloader import DummySpiderLoader, SpiderLoader, get_spider_loader

module_dir = Path(__file__).resolve().parent


def _copytree(source: Path, target: Path):
    with contextlib.suppress(shutil.Error):
        shutil.copytree(source, target)


@pytest.fixture
def spider_loader_env(tmp_path):
    orig_spiders_dir = module_dir / "test_spiders"
    spiders_dir = tmp_path / "test_spiders_xxx"
    _copytree(orig_spiders_dir, spiders_dir)
    sys.path.append(str(tmp_path))
    settings = Settings({"SPIDER_MODULES": ["test_spiders_xxx"]})

    yield settings, spiders_dir

    sys.modules.pop("test_spiders_xxx", None)
    sys.path.remove(str(tmp_path))


@pytest.fixture
def spider_loader(spider_loader_env):
    settings, _ = spider_loader_env
    return SpiderLoader.from_settings(settings)


class TestSpiderLoader:
    def test_list(self, spider_loader):
        assert set(spider_loader.list()) == {
            "spider1",
            "spider2",
            "spider3",
            "spider4",
        }

    def test_load(self, spider_loader):
        spider1 = spider_loader.load("spider1")
        assert spider1.__name__ == "Spider1"

    def test_find_by_request(self, spider_loader):
        assert spider_loader.find_by_request(Request("http://scrapy1.org/test")) == [
            "spider1"
        ]
        assert spider_loader.find_by_request(Request("http://scrapy2.org/test")) == [
            "spider2"
        ]
        assert set(
            spider_loader.find_by_request(Request("http://scrapy3.org/test"))
        ) == {"spider1", "spider2"}
        assert spider_loader.find_by_request(Request("http://scrapy999.org/test")) == []
        assert spider_loader.find_by_request(Request("http://spider3.com")) == []
        assert spider_loader.find_by_request(
            Request("http://spider3.com/onlythis")
        ) == ["spider3"]

    def test_load_spider_module(self):
        module = "tests.test_spiderloader.test_spiders.spider1"
        settings = Settings({"SPIDER_MODULES": [module]})
        spider_loader = SpiderLoader.from_settings(settings)
        assert len(spider_loader._spiders) == 1

    def test_load_spider_module_multiple(self):
        prefix = "tests.test_spiderloader.test_spiders."
        module = ",".join(prefix + s for s in ("spider1", "spider2"))
        settings = Settings({"SPIDER_MODULES": module})
        spider_loader = SpiderLoader.from_settings(settings)
        assert len(spider_loader._spiders) == 2

    def test_load_base_spider(self):
        module = "tests.test_spiderloader.test_spiders.spider0"
        settings = Settings({"SPIDER_MODULES": [module]})
        spider_loader = SpiderLoader.from_settings(settings)
        assert len(spider_loader._spiders) == 0

    def test_load_spider_module_from_addons(self):
        module = "tests.test_spiderloader.spiders_from_addons.spider0"

        class SpiderModuleAddon:
            @classmethod
            def update_pre_crawler_settings(cls, settings):
                settings.set(
                    "SPIDER_MODULES",
                    [module],
                    "project",
                )

        runner = CrawlerRunner({"ADDONS": {SpiderModuleAddon: 1}})

        crawler = runner.create_crawler("spider_from_addon")
        assert issubclass(crawler.spidercls, scrapy.Spider)
        assert crawler.spidercls.name == "spider_from_addon"
        assert len(crawler.settings["SPIDER_MODULES"]) == 1

    def test_crawler_runner_loading(self):
        module = "tests.test_spiderloader.test_spiders.spider1"
        runner = CrawlerRunner(
            {
                "SPIDER_MODULES": [module],
            }
        )

        with pytest.raises(KeyError, match="Spider not found"):
            runner.create_crawler("spider2")

        crawler = runner.create_crawler("spider1")
        assert issubclass(crawler.spidercls, scrapy.Spider)
        assert crawler.spidercls.name == "spider1"

    def test_bad_spider_modules_exception(self):
        module = "tests.test_spiderloader.test_spiders.doesnotexist"
        settings = Settings({"SPIDER_MODULES": [module]})
        with pytest.raises(ImportError):
            SpiderLoader.from_settings(settings)

    def test_bad_spider_modules_warning(self):
        module = "tests.test_spiderloader.test_spiders.doesnotexist"
        settings = Settings(
            {"SPIDER_MODULES": [module], "SPIDER_LOADER_WARN_ONLY": True}
        )
        with pytest.warns(RuntimeWarning, match="Could not load spiders from module"):
            spider_loader = SpiderLoader.from_settings(settings)

        spiders = spider_loader.list()
        assert not spiders

    def test_syntax_error_exception(self):
        module = "tests.test_spiderloader.test_spiders.spider1"
        settings = Settings({"SPIDER_MODULES": [module]})
        with mock.patch.object(SpiderLoader, "_load_spiders") as m:
            m.side_effect = SyntaxError
            with pytest.raises(SyntaxError):
                SpiderLoader.from_settings(settings)

    def test_syntax_error_warning(self):
        module = "tests.test_spiderloader.test_spiders.spider1"
        settings = Settings(
            {"SPIDER_MODULES": [module], "SPIDER_LOADER_WARN_ONLY": True}
        )
        with mock.patch.object(SpiderLoader, "_load_spiders") as m:
            m.side_effect = SyntaxError
            with pytest.warns(
                RuntimeWarning, match="Could not load spiders from module"
            ):
                spider_loader = SpiderLoader.from_settings(settings)

        spiders = spider_loader.list()
        assert not spiders


class TestDuplicateSpiderNameLoader:
    def test_dupename_warning(self, spider_loader_env):
        settings, spiders_dir = spider_loader_env

        # copy 1 spider module so as to have duplicate spider name
        shutil.copyfile(spiders_dir / "spider3.py", spiders_dir / "spider3dupe.py")

        msg = r"""There are several spiders with the same name:

  Spider3 named 'spider3' \(in test_spiders_xxx\.spider3\)

  Spider3 named 'spider3' \(in test_spiders_xxx\.spider3dupe\)

  This can cause unexpected behavior\."""
        with pytest.warns(UserWarning, match=msg):
            spider_loader = SpiderLoader.from_settings(settings)
        spiders = set(spider_loader.list())
        assert spiders == {"spider1", "spider2", "spider3", "spider4"}

    def test_multiple_dupename_warning(self, spider_loader_env):
        settings, spiders_dir = spider_loader_env
        # copy 2 spider modules so as to have duplicate spider name
        shutil.copyfile(spiders_dir / "spider1.py", spiders_dir / "spider1dupe.py")
        shutil.copyfile(spiders_dir / "spider2.py", spiders_dir / "spider2dupe.py")

        msg = r"""There are several spiders with the same name:

  Spider1 named 'spider1' \(in test_spiders_xxx\.spider1\)

  Spider1 named 'spider1' \(in test_spiders_xxx\.spider1dupe\)

  Spider2 named 'spider2' \(in test_spiders_xxx\.spider2\)

  Spider2 named 'spider2' \(in test_spiders_xxx\.spider2dupe\)

  This can cause unexpected behavior\."""
        with pytest.warns(UserWarning, match=msg):
            spider_loader = SpiderLoader.from_settings(settings)
        spiders = set(spider_loader.list())
        assert spiders == {"spider1", "spider2", "spider3", "spider4"}


class CustomSpiderLoader(SpiderLoader):
    pass


def test_custom_spider_loader():
    settings = Settings(
        {
            "SPIDER_LOADER_CLASS": CustomSpiderLoader,
        }
    )
    spider_loader = get_spider_loader(settings)
    assert isinstance(spider_loader, CustomSpiderLoader)


def test_dummy_spider_loader(spider_loader_env):
    settings, _ = spider_loader_env
    spider_loader = DummySpiderLoader.from_settings(settings)
    assert not spider_loader.list()
    with pytest.raises(KeyError):
        spider_loader.load("spider1")
    assert not spider_loader.find_by_request(Request("http://example.com"))
