import stat
import unittest
import yaml
from collections import namedtuple
from unittest.mock import patch
from parameterized import parameterized

import __res as res


NAMESPACE_PREFIX = b"py/namespace/"
TEST_SOURCE_ROOT = "/home/arcadia"


class ImporterMocks:
    def __init__(self, mock_fs, mock_resources):
        self._mock_fs = mock_fs
        self._mock_resources = mock_resources
        self._patchers = [
            patch("__res.iter_keys", wraps=self._iter_keys),
            patch("__res.__resource.find", wraps=self._resource_find),
            patch("__res._path_isfile", wraps=self._path_isfile),
            patch("__res._os.listdir", wraps=self._os_listdir),
            patch("__res._os.lstat", wraps=self._os_lstat),
        ]
        for patcher in self._patchers:
            patcher.start()

    def stop(self):
        for patcher in self._patchers:
            patcher.stop()

    def _iter_keys(self, prefix):
        assert prefix == NAMESPACE_PREFIX
        for k in self._mock_resources.keys():
            yield k, k.removeprefix(prefix)

    def _resource_find(self, key):
        return self._mock_resources.get(key)

    def _lookup_mock_fs(self, filename):
        path = filename.lstrip("/").split("/")
        curdir = self._mock_fs
        for item in path:
            if item in curdir:
                curdir = curdir[item]
            else:
                return None
        return curdir

    def _path_isfile(self, filename):
        f = self._lookup_mock_fs(filename)
        return isinstance(f, str)

    def _os_lstat(self, filename):
        f = self._lookup_mock_fs(filename)
        mode = stat.S_IFDIR if isinstance(f, dict) else stat.S_IFREG
        return namedtuple("fake_stat_type", "st_mode")(st_mode=mode)

    def _os_listdir(self, dirname):
        f = self._lookup_mock_fs(dirname)
        if isinstance(f, dict):
            return f.keys()
        else:
            return []


class ArcadiaSourceFinderTestCase(unittest.TestCase):
    def setUp(self):
        self.import_mock = ImporterMocks(yaml.safe_load(self._get_mock_fs()), self._get_mock_resources())
        self.arcadia_source_finder = res.ArcadiaSourceFinder(TEST_SOURCE_ROOT)

    def tearDown(self):
        self.import_mock.stop()

    def _get_mock_fs(self):
        raise NotImplementedError()

    def _get_mock_resources(self):
        raise NotImplementedError()


class TestLibraryWithoutNamespace(ArcadiaSourceFinderTestCase):
    def _get_mock_fs(self):
        return """
           home:
             arcadia:
               project:
                 lib:
                   mod1.py: ""
                   package1:
                     mod2.py: ""
        """

    def _get_mock_resources(self):
        return {
            b"py/namespace/unique_prefix1/project/lib": b"project.lib.",
        }

    @parameterized.expand(
        [
            ("project.lib.mod1", b"project/lib/mod1.py"),
            ("project.lib.package1.mod2", b"project/lib/package1/mod2.py"),
            ("project.lib.unknown_module", None),
            ("project.lib", None),  # package
        ]
    )
    def test_get_module_path(self, module, path):
        assert path == self.arcadia_source_finder.get_module_path(module)

    @parameterized.expand(
        [
            ("project.lib.mod1", False),
            ("project.lib.package1.mod2", False),
            ("project", True),
            ("project.lib", True),
            ("project.lib.package1", True),
        ]
    )
    def test_is_packages(self, module, is_package):
        assert is_package == self.arcadia_source_finder.is_package(module)

    def test_is_package_for_unknown_module(self):
        self.assertRaises(
            ImportError,
            lambda: self.arcadia_source_finder.is_package("project.lib.package2"),
        )

    @parameterized.expand(
        [
            (
                "",
                {
                    ("PFX.project", True),
                },
            ),
            (
                "project.",
                {
                    ("PFX.lib", True),
                },
            ),
            (
                "project.lib.",
                {
                    ("PFX.mod1", False),
                    ("PFX.package1", True),
                },
            ),
            (
                "project.lib.package1.",
                {
                    ("PFX.mod2", False),
                },
            ),
        ]
    )
    def test_iter_modules(self, package_prefix, expected):
        got = self.arcadia_source_finder.iter_modules(package_prefix, "PFX.")
        assert expected == set(got)

    # Check iter_modules() don't crash and return correct result after not existing module was requested
    def test_iter_modules_after_unknown_module_import(self):
        self.arcadia_source_finder.get_module_path("project.unknown_module")
        assert {("lib", True)} == set(self.arcadia_source_finder.iter_modules("project.", ""))


class TestLibraryExtendedFromAnotherLibrary(ArcadiaSourceFinderTestCase):
    def _get_mock_fs(self):
        return """
           home:
             arcadia:
               project:
                 lib:
                   mod1.py: ''
                 lib_extension:
                   mod2.py: ''
        """

    def _get_mock_resources(self):
        return {
            b"py/namespace/unique_prefix1/project/lib": b"project.lib.",
            b"py/namespace/unique_prefix2/project/lib_extension": b"project.lib.",
        }

    @parameterized.expand(
        [
            ("project.lib.mod1", b"project/lib/mod1.py"),
            ("project.lib.mod2", b"project/lib_extension/mod2.py"),
        ]
    )
    def test_get_module_path(self, module, path):
        assert path == self.arcadia_source_finder.get_module_path(module)

    @parameterized.expand(
        [
            (
                "project.lib.",
                {
                    ("PFX.mod1", False),
                    ("PFX.mod2", False),
                },
            ),
        ]
    )
    def test_iter_modules(self, package_prefix, expected):
        got = self.arcadia_source_finder.iter_modules(package_prefix, "PFX.")
        assert expected == set(got)


class TestNamespaceAndTopLevelLibraries(ArcadiaSourceFinderTestCase):
    def _get_mock_fs(self):
        return """
           home:
             arcadia:
               project:
                 ns_lib:
                   mod1.py: ''
                 top_level_lib:
                   mod2.py: ''
        """

    def _get_mock_resources(self):
        return {
            b"py/namespace/unique_prefix1/project/ns_lib": b"ns.",
            b"py/namespace/unique_prefix2/project/top_level_lib": b".",
        }

    @parameterized.expand(
        [
            ("ns.mod1", b"project/ns_lib/mod1.py"),
            ("mod2", b"project/top_level_lib/mod2.py"),
        ]
    )
    def test_get_module_path(self, module, path):
        assert path == self.arcadia_source_finder.get_module_path(module)

    @parameterized.expand(
        [
            ("ns", True),
            ("ns.mod1", False),
            ("mod2", False),
        ]
    )
    def test_is_packages(self, module, is_package):
        assert is_package == self.arcadia_source_finder.is_package(module)

    @parameterized.expand(
        [
            "project",
            "project.ns_lib",
            "project.top_level_lib",
        ]
    )
    def test_is_package_for_unknown_modules(self, module):
        self.assertRaises(ImportError, lambda: self.arcadia_source_finder.is_package(module))

    @parameterized.expand(
        [
            (
                "",
                {
                    ("PFX.ns", True),
                    ("PFX.mod2", False),
                },
            ),
            (
                "ns.",
                {
                    ("PFX.mod1", False),
                },
            ),
        ]
    )
    def test_iter_modules(self, package_prefix, expected):
        got = self.arcadia_source_finder.iter_modules(package_prefix, "PFX.")
        assert expected == set(got)


class TestIgnoreDirectoriesWithYaMakeFile(ArcadiaSourceFinderTestCase):
    """Packages and modules from tests should not be part of pylib namespace"""

    def _get_mock_fs(self):
        return """
           home:
             arcadia:
               contrib:
                 python:
                   pylib:
                     mod1.py: ""
                     tests:
                       conftest.py: ""
                       ya.make: ""
        """

    def _get_mock_resources(self):
        return {
            b"py/namespace/unique_prefix1/contrib/python/pylib": b"pylib.",
        }

    def test_get_module_path_for_lib(self):
        assert b"contrib/python/pylib/mod1.py" == self.arcadia_source_finder.get_module_path("pylib.mod1")

    def test_get_module_for_tests(self):
        assert self.arcadia_source_finder.get_module_path("pylib.tests.conftest") is None

    def test_is_package_for_tests(self):
        self.assertRaises(ImportError, lambda: self.arcadia_source_finder.is_package("pylib.tests"))


class TestMergingNamespaceAndDirectoryPackages(ArcadiaSourceFinderTestCase):
    """Merge parent package (top level in this test) dirs with namespace dirs (DEVTOOLS-8979)"""

    def _get_mock_fs(self):
        return """
           home:
             arcadia:
               contrib:
                 python:
                   pylint:
                     ya.make: ""
                     pylint:
                       __init__.py: ""
                     patcher:
                       patch.py: ""
                       ya.make: ""
        """

    def _get_mock_resources(self):
        return {
            b"py/namespace/unique_prefix1/contrib/python/pylint": b".",
            b"py/namespace/unique_prefix1/contrib/python/pylint/patcher": b"pylint.",
        }

    @parameterized.expand(
        [
            ("pylint.__init__", b"contrib/python/pylint/pylint/__init__.py"),
            ("pylint.patch", b"contrib/python/pylint/patcher/patch.py"),
        ]
    )
    def test_get_module_path(self, module, path):
        assert path == self.arcadia_source_finder.get_module_path(module)


class TestEmptyResources(ArcadiaSourceFinderTestCase):
    def _get_mock_fs(self):
        return """
           home:
             arcadia:
               project:
                 lib:
                   mod1.py: ''
        """

    def _get_mock_resources(self):
        return {}

    def test_get_module_path(self):
        assert self.arcadia_source_finder.get_module_path("project.lib.mod1") is None

    def test_is_package(self):
        self.assertRaises(ImportError, lambda: self.arcadia_source_finder.is_package("project"))

    def test_iter_modules(self):
        assert [] == list(self.arcadia_source_finder.iter_modules("", "PFX."))


class TestDictionaryChangedSizeDuringIteration(ArcadiaSourceFinderTestCase):
    def _get_mock_fs(self):
        return """
           home:
             arcadia:
               project:
                 lib1:
                   mod1.py: ''
                 lib2:
                   mod2.py: ''
        """

    def _get_mock_resources(self):
        return {
            b"py/namespace/unique_prefix1/project/lib1": b"project.lib1.",
            b"py/namespace/unique_prefix1/project/lib2": b"project.lib2.",
        }

    def test_no_crash_on_recusive_iter_modules(self):
        for package in self.arcadia_source_finder.iter_modules("project.", ""):
            for _ in self.arcadia_source_finder.iter_modules(package[0], ""):
                pass
